# User Access Control

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Routing
Source: https://www.devtutorials.ai/learn/react/routing/user-access-control

> Authentication and authorization with JWT: the login flow, user identity and roles, role-based access control, protected routes and guards, an auth context and auth service, login state, logout and best practices.

## What Is Authentication?

**Authentication** means checking **who** a user is. The most common way is a login form: the user types an email and a password, and the server checks them.

If they are correct, the user is **logged in**. The app now knows who is using it.

### Real-World Example

In a bookshop app, the user signs in with an email and a password. The app checks them and remembers who the user is.

A guest who opens the **My orders** page is sent to the sign-in page. A normal customer does not see the **Admin** page in the menu, because only a user with the admin role may open it.

Authentication answers one question: **Who are you?**

## What Is Authorization?

**Authorization** means checking **what** a logged-in user is allowed to do. For example, a customer can see their own orders, but only an admin can edit the book list.

Authorization always comes **after** authentication. You must know who the user is before you can decide what they may do.

Authorization answers a different question: **What are you allowed to do?**

## Authentication vs Authorization

<figure className="react-fig">
<svg viewBox="0 0 520 160" role="img" aria-labelledby="fig-a-authnz" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-a-authnz">Authentication asks who you are. Authorization asks what you are allowed to do</title>
  <rect x="10" y="20" width="230" height="56" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="125.0" y="45.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Authentication</text>
  <text x="125.0" y="62.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>Who are you?</text>
  <text x="125" y="100" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>Check the email and password</text>
  <text x="125" y="118" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 12, fontWeight: 600, fontFamily: "var(--sans)" }}>→ "You are Asha"</text>
  <path d="M240 48 L280 48" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M280 48 L271.7 51.5 L271.7 44.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="285" y="20" width="225" height="56" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="397.5" y="45.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Authorization</text>
  <text x="397.5" y="62.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>What can you do?</text>
  <text x="397" y="100" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>Check the role</text>
  <text x="397" y="118" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 12, fontWeight: 600, fontFamily: "var(--sans)" }}>→ "Asha may see orders, not admin"</text>
  <text x="260" y="150" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 12, fontWeight: 600, fontFamily: "var(--sans)" }}>Always in this order: first who, then what.</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="1.6s" repeatCount="indefinite" path="M240 48 L280 48" />
  </circle>
</svg>
<figcaption>Authentication comes first. Authorization uses the result to decide what is allowed.</figcaption>
</figure>

<div className="tscroll">

| | Authentication | Authorization |
|---|---|---|
| Question | Who are you? | What can you do? |
| When | First, at login | After login, on every protected action |
| Checks | Email and password (or Google, OTP, ...) | The user's role or permissions |
| If it fails | "Wrong email or password" | "You do not have access to this page" |
| Example | Asha logs in | Asha may see her orders, but not the admin page |

</div>

## Authentication Flow Overview

Here is what happens when a user logs in to a React app that uses **JWT**.

A **JWT** (JSON Web Token, said "jot") is a long piece of text that the server gives the app after a correct login. The app sends it with each request, so the server knows who the user is without asking for the password again.

<figure className="react-fig">
<svg viewBox="0 0 520 200" role="img" aria-labelledby="fig-a-flow" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-a-flow">Login flow: the user sends email and password, the server checks them and returns a JWT, the app keeps it and sends it with each request</title>
  <rect x="5" y="20" width="110" height="48" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="60.0" y="41.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>User</text>
  <text x="60.0" y="58.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>types details</text>
  <path d="M115 44 L150 44" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M150 44 L141.7 47.5 L141.7 40.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="155" y="20" width="110" height="48" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="210.0" y="41.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Login form</text>
  <text x="210.0" y="58.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>onSubmit</text>
  <path d="M265 44 L300 44" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M300 44 L291.7 47.5 L291.7 40.5 Z" style={{ fill: "var(--faint)" }} />
  <text x="282.5" y="36" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>sends</text>
  <rect x="305" y="20" width="110" height="48" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="360.0" y="41.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Auth server</text>
  <text x="360.0" y="58.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>checks password</text>
  <path d="M360 68 L360 108" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 108 L356.5 99.7 L363.5 99.7 Z" style={{ fill: "var(--faint)" }} />
  <text x="360.0" y="92" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>JWT</text>
  <rect x="305" y="110" width="110" height="48" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="360.0" y="131.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>App</text>
  <text x="360.0" y="148.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>keeps the token</text>
  <path d="M305 134 L270 134" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M270 134 L278.3 130.5 L278.3 137.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="150" y="110" width="115" height="48" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="207.5" y="131.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>AuthContext</text>
  <text x="207.5" y="148.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>user = Asha</text>
  <path d="M415 134 L450 134" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M450 134 L441.7 137.5 L441.7 130.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="455" y="110" width="60" height="48" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="485.0" y="139.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>API</text>
  <text x="485" y="175" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 10, fontWeight: 400, fontFamily: "var(--sans)" }}>token sent</text>
  <text x="485" y="188" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 10, fontWeight: 400, fontFamily: "var(--sans)" }}>with requests</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="5s" repeatCount="indefinite" path="M115 44 L150 44 M265 44 L300 44 M360 68 L360 108 M305 134 L270 134 M415 134 L450 134" />
  </circle>
</svg>
<figcaption>After login, the app keeps a token. It proves who the user is, without sending the password again.</figcaption>
</figure>

1. The user types an email and password in the login form.
2. The app sends them to the server's login API.
3. The server checks them. If they are correct, it creates a JWT and sends it back.
4. The app keeps the token, and stores the user's details in state.
5. For every later request, the app sends the token along. The server checks the token and answers.

### What Is Inside a JWT?

A JWT has three parts, joined by dots.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-a-jwt" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-a-jwt">A JWT has three parts separated by dots: header, payload and signature</title>
  <text x="260" y="30" textAnchor="middle" style={{ fontSize: 15, fontFamily: "var(--mono)" }}><tspan style={{ fill: "var(--accent)", fontWeight: 700 }}>eyJhbGci</tspan><tspan style={{ fill: "var(--ink2)" }}>.</tspan><tspan style={{ fill: "var(--accent-secondary)", fontWeight: 700 }}>eyJzdWIiOiIxIi...</tspan><tspan style={{ fill: "var(--ink2)" }}>.</tspan><tspan style={{ fill: "var(--ok)", fontWeight: 700 }}>SflKxwRJ</tspan></text>
  <path d="M150 40 L90 78" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M90 78 L95.1 70.6 L98.9 76.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M260 40 L260 78" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M260 78 L256.5 69.7 L263.5 69.7 Z" style={{ fill: "var(--faint)" }} />
  <path d="M370 40 L430 78" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M430 78 L421.1 76.5 L424.9 70.6 Z" style={{ fill: "var(--faint)" }} />
  <rect x="10" y="80" width="160" height="74" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="90.0" y="114.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Header</text>
  <text x="90.0" y="131.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>which algorithm</text>
  <rect x="180" y="80" width="160" height="74" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="114.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Payload</text>
  <text x="260.0" y="131.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>sub, name, role, exp</text>
  <rect x="350" y="80" width="160" height="74" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="430.0" y="114.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Signature</text>
  <text x="430.0" y="131.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>made with the server's secret</text>
</svg>
<figcaption>The payload holds the user's details. The signature proves the server made the token.</figcaption>
</figure>

- **Header** — says how the token was signed.
- **Payload** — the user's details, called **claims**. For example: `sub` (the user's id), `name`, `role`, and `exp` (when the token expires).
- **Signature** — made by the server with a secret key. If anyone changes the payload, the signature no longer matches, and the server rejects the token.

The header and payload are only **encoded**, not hidden. Anyone with the token can read them. So never put a password or other secrets in a JWT.

## User Identity

**User identity** is the information that says who the logged-in user is. In a JWT app, the identity comes from the token's payload.

The lab below has a pretend **auth service**. It acts like a server: it checks the password and creates a JWT-shaped token. Then the app reads the payload back to learn who logged in.

      <button onClick={() => tryLogin("asha@bookcorner.com", "asha123")}>Log in as Asha</button>{" "}
      <button onClick={() => tryLogin("asha@bookcorner.com", "wrong")}>Wrong password</button>
      {error && <p className="error">{error}</p>}
      {token && (
        <div>
          <p className="token">Token: {token}</p>
          <p>Name: {user?.name} · Role: {user?.role} · User id: {user?.sub}</p>
        </div>
      )}
    </div>
  );
}
`,
    "App.css": `.token { word-break: break-all; font-family: monospace; font-size: 12px; background: #f1f5f9; padding: 6px; border-radius: 6px; }
.error { color: #dc2626; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click **Log in as Asha**. After half a second, the token appears: three parts joined by dots. Below it, the app shows the name and role it read from the payload. Click **Wrong password**, and you get an error instead of a token.

In a real app, the browser cannot create a real JWT, because it does not have the server's secret key. The pretend service on this page is only for learning. The signature here is fake on purpose.

## User Roles

A **role** is a group name for users who may do the same things. Most apps have only a few roles, for example:

- **customer** — can view books and place orders.
- **admin** — can do everything a customer can, and also edit books and see all orders.

The role is usually part of the JWT payload, so the app knows it right after login: `role: "admin"`.

## Role-Based Access Control (RBAC)

**Role-based access control** (RBAC) means you give permissions to **roles**, not to each person. A user can do something if their role allows it.

<figure className="react-fig">
<svg viewBox="0 0 520 160" role="img" aria-labelledby="fig-a-rbac" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-a-rbac">Role-based access control: users get roles, and roles get permissions</title>
  <rect x="5" y="20" width="110" height="40" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="60.0" y="45.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Asha</text>
  <rect x="5" y="100" width="110" height="40" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="60.0" y="125.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Ravi</text>
  <path d="M115 40 L165 40" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M165 40 L156.7 43.5 L156.7 36.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M115 120 L165 120" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M165 120 L156.7 123.5 L156.7 116.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="170" y="20" width="140" height="40" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="240.0" y="45.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>customer</text>
  <rect x="170" y="100" width="140" height="40" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="240.0" y="125.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>admin</text>
  <path d="M310 40 L360 30" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 30 L352.6 35.1 L351.2 28.2 Z" style={{ fill: "var(--faint)" }} />
  <path d="M310 40 L360 70" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 70 L351.1 68.7 L354.7 62.7 Z" style={{ fill: "var(--faint)" }} />
  <path d="M310 120 L360 30" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 30 L359.0 38.9 L352.9 35.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M310 120 L360 70" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 70 L356.6 78.3 L351.7 73.4 Z" style={{ fill: "var(--faint)" }} />
  <path d="M310 120 L360 110" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 110 L352.6 115.1 L351.2 108.2 Z" style={{ fill: "var(--faint)" }} />
  <path d="M310 120 L360 148" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 148 L351.1 147.0 L354.5 140.9 Z" style={{ fill: "var(--faint)" }} />
  <rect x="365" y="12" width="150" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="440.0" y="32.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>view books</text>
  <rect x="365" y="55" width="150" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="440.0" y="75.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>place orders</text>
  <rect x="365" y="95" width="150" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="440.0" y="115.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>edit books</text>
  <rect x="365" y="133" width="150" height="26" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="440.0" y="151.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>see all orders</text>
</svg>
<figcaption>You check the role, not the person. Change a role's permissions once, and every user with that role is updated.</figcaption>
</figure>

A simple way to write RBAC is a list of permissions for each role, and one function to check it.

```tsx
const permissions = {
  customer: ["view-books", "place-order"],
  admin: ["view-books", "place-order", "edit-books", "see-all-orders"],
};

function can(role: Role, action: string): boolean {
  return permissions[role].includes(action);
}

can("customer", "edit-books"); // false
can("admin", "edit-books");    // true
```

Now the app asks `can(user.role, "edit-books")` instead of checking names. If the shop adds a new "manager" role, you only add one line to the list.

      <label>
        Role:{" "}
        <select value={role} onChange={(e) => setRole(e.target.value as Role)}>
          <option value="customer">customer</option>
          <option value="manager">manager</option>
          <option value="admin">admin</option>
        </select>
      </label>
      <ul>
        {actions.map((action) => (
          <li key={action}>
            {can(role, action) ? "✅" : "❌"} {action}
          </li>
        ))}
      </ul>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Change the role. The list of allowed actions changes at once. The "manager" role was added with just one line in `permissions`.

## Protected Routes

A **protected route** is a page that only some users may open. For example, `/orders` needs a logged-in user, and `/admin` needs an admin.

If the user may not open it, the app sends them somewhere else, usually to the login page. This is called a **redirect**.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-a-guard" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-a-guard">A protected route: if the user is logged in, show the page. If not, redirect to the login page</title>
  <rect x="10" y="62" width="130" height="46" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="75.0" y="82.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>/orders</text>
  <text x="75.0" y="99.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>user asks for</text>
  <path d="M140 85 L180 85" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M180 85 L171.7 88.5 L171.7 81.5 Z" style={{ fill: "var(--faint)" }} />
  <polygon points="250,40 320,85 250,130 180,85" style={{ fill: "var(--panel)", stroke: "var(--ink2)", strokeWidth: 1.5 }} />
  <text x="250" y="82" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>logged</text>
  <text x="250" y="97" textAnchor="middle" style={{ fill: "var(--ink)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>in?</text>
  <path d="M300 72 L360 35" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 35 L354.8 42.3 L351.1 36.4 Z" style={{ fill: "var(--faint)" }} />
  <text x="330.0" y="44" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>yes</text>
  <rect x="365" y="12" width="150" height="46" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="440.0" y="32.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Orders page</text>
  <text x="440.0" y="49.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>shown</text>
  <path d="M300 98 L360 135" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M360 135 L351.1 133.6 L354.8 127.7 Z" style={{ fill: "var(--faint)" }} />
  <text x="330.0" y="130" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>no</text>
  <rect x="365" y="112" width="150" height="46" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="440.0" y="132.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>Redirect</text>
  <text x="440.0" y="149.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>to /login</text>
  <circle r="6" style={{ fill: "var(--bad)" }}>
    <animateMotion dur="2.4s" repeatCount="indefinite" path="M140 85 L180 85 M300 98 L360 135" />
  </circle>
</svg>
<figcaption>The guard sits in front of the page. Only logged-in users get through.</figcaption>
</figure>

## Route Guards Pattern in React

A **route guard** is a component that wraps a protected page. It checks the user first:

- If the check passes, it shows the page (`children`).
- If not, it redirects with React Router's `<Navigate>` component.

```tsx
function RequireAuth({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  return children;
}

// Use it around the page:
<Route path="/orders" element={<RequireAuth><OrdersPage /></RequireAuth>} />
```

`<Navigate to="/login" replace />` changes the page as soon as it is shown. `replace` means the blocked page is not saved in the history, so the Back button does not lead back to it.

      <nav>
      </nav>
        <Route path="/" element={<h3>Home</h3>} />
        <Route path="/login" element={<h3>Please log in to see your orders.</h3>} />
        <Route path="/orders" element={<RequireAuth loggedIn={loggedIn}><h3>Your orders: #1021 Atomic Habits</h3></RequireAuth>} />
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click **My orders** while logged out. The address changes to `/login`. Now click **Log in**, then **My orders** again. This time the guard lets you through.

## Authentication Context Pattern

Many components need to know who is logged in: the header, the guards, the account page. So the login state belongs in **context**, which you learned in the Context API lesson.

The pattern has three parts:

- an **AuthContext** that holds the user, plus `login` and `logout` functions;
- an **AuthProvider** component near the top of the app;
- a **useAuth** hook, so any component can read it.

## Auth Service Pattern

An **auth service** is a separate file that does the talking to the server: log in, read the token, check if it has expired. Components and context call the service. They do not contain server code themselves.

- **Components** show the screen.
- **AuthContext** remembers who is logged in.
- **authService** talks to the server and handles tokens.

This keeps each file small, and makes it easy to change the server code later without touching the screens.

## Login State Management

The login state is the data that says who is logged in. Keep it small:

- `user` — the details from the token payload, or `null` when nobody is logged in.
- `token` — the JWT, needed to call the server.

From `user`, you can work out everything else: `isLoggedIn` is just `user !== null`. Also check the token's `exp` time. An expired token means the user must log in again.

      {isLoggedIn ? (
        <p>Hi, {session!.name}. Your token expires in {secondsLeft} seconds.</p>
      ) : (
        <p>{session ? "Your session has expired. Please log in again." : "You are logged out."}</p>
      )}
      <button onClick={() => setSession({ name: "Asha", exp: Date.now() + 10000 })}>Log in</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click **Log in** and wait. The seconds count down. At 0, the app treats the user as logged out, even though nobody pressed Log out. Real tokens usually last minutes or hours, not seconds.

Here is the full pattern together: an auth service, an auth context, a login page, route guards, and a role check.

  btoa(JSON.stringify(part)).replace(/=+$/, "").replace(/\\+/g, "-").replace(/\\//g, "_");

// Pretend server: checks the password and returns a JWT-shaped token.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const u = users.find((x) => x.email === email && x.password === password);
      if (!u) return reject(new Error("Wrong email or password."));
      const payload = { sub: u.id, name: u.name, role: u.role, exp: Math.floor(Date.now() / 1000) + 3600 };
      resolve(encode({ alg: "HS256", typ: "JWT" }) + "." + encode(payload) + ".demo-signature");
    }, 400);
  });
}

  const p = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
  return { id: p.sub, name: p.name, role: p.role, exp: p.exp };
}

  return user.exp * 1000 < Date.now();
}

const permissions: Record<Role, string[]> = {
  customer: ["view-books", "see-own-orders"],
  admin: ["view-books", "see-own-orders", "edit-books"],
};

  return permissions[role].includes(action);
}
`,
    "AuthContext.tsx": `import { createContext, useContext, useState } from "react";

interface AuthValue {
  user: User | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthValue | null>(null);

  const [token, setToken] = useState<string | null>(null);

  // Work the user out from the token. An expired token counts as logged out.
  const decoded = token ? authService.readUser(token) : null;
  const user = decoded && !authService.isExpired(decoded) ? decoded : null;

  async function login(email: string, password: string) {
    const newToken = await authService.login(email, password); // throws if wrong
    setToken(newToken);
  }

  function logout() {
    setToken(null);
  }

  return (
      {children}
  );
}

  const value = useContext(AuthContext);
  if (!value) throw new Error("useAuth must be used inside <AuthProvider>");
  return value;
}
`,
    "Guards.tsx": `import { Navigate, useLocation } from "react-router-dom";

// Only logged-in users get through.
  const { user } = useAuth();
  const location = useLocation();
  if (!user) {
    // Remember where the user wanted to go, so we can send them back after login.
    return <Navigate to="/login" replace state={{ from: location.pathname }} />;
  }
  return <>{children}</>;
}

// Only users whose role allows this action get through.
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  if (!can(user.role, action)) return <p className="error">Sorry, you do not have access to this page.</p>;
  return <>{children}</>;
}
`,
    "LoginPage.tsx": `import { useState } from "react";

  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  const [email, setEmail] = useState("asha@bookcorner.com");
  const [password, setPassword] = useState("asha123");
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setError("");
    setBusy(true);
    try {
      await login(email, password);
      const from = (location.state as { from?: string } | null)?.from ?? "/";
      navigate(from, { replace: true }); // go back to the page the user wanted
    } catch (err) {
      setError((err as Error).message);
    }
    setBusy(false);
  }

  return (
    <form className="form" onSubmit={handleSubmit}>
      <h3>Log in</h3>
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
      <button type="submit" disabled={busy}>{busy ? "Checking..." : "Log in"}</button>
      {error && <p className="error">{error}</p>}
      <small>Try ravi@bookcorner.com / ravi123 for an admin.</small>
    </form>
  );
}
`,
    "App.tsx": `import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function Header() {
  const { user, logout } = useAuth();
  return (
    <nav className="nav">
      <Link to="/">Books</Link>
      <Link to="/orders">My orders</Link>
      {user && can(user.role, "edit-books") && <Link to="/admin">Admin</Link>}
      <span className="who">
        {user ? (
          <>Hi, {user.name} ({user.role}) <button onClick={logout}>Log out</button></>
        ) : (
          <Link to="/login">Log in</Link>
        )}
      </span>
    </nav>
  );
}

  return (
          <Route path="/" element={<p>Everyone can see the books.</p>} />
          <Route path="/login" element={<LoginPage />} />
          <Route path="/orders" element={<RequireAuth><p>Your orders: #1021 Atomic Habits</p></RequireAuth>} />
          <Route path="/admin" element={<RequirePermission action="edit-books"><p>Admin: edit the book list here.</p></RequirePermission>} />
  );
}
`,
    "App.css": `.nav { display: flex; gap: 10px; align-items: center; border-bottom: 1px solid #e2e8f0; padding-bottom: 6px; font-size: 14px; }
.nav .who { margin-left: auto; }
.form { display: grid; gap: 6px; max-width: 260px; }
.form h3 { margin: 4px 0; }
.error { color: #dc2626; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Try these steps, and watch the address bar:

1. Click **My orders**. You are not logged in, so `RequireAuth` sends you to `/login`.
2. Press **Log in** (Asha's details are filled in). You go back to `/orders`, the page you wanted.
3. Asha is a **customer**, so there is no **Admin** link. Log out, and log in as Ravi. Now the **Admin** link appears.

Click through the file tabs. Each file has one job: `authService` talks to the "server", `AuthContext` remembers the user, `Guards` protect the routes, and `LoginPage` shows the form.

## Conditional Rendering Based on Authentication

Once you have `user` from context, showing different things is just conditional rendering, which you learned earlier.

```tsx
const { user } = useAuth();

{user ? <p>Hi, {user.name}</p> : <Link to="/login">Log in</Link>}
{user && can(user.role, "edit-books") && <Link to="/admin">Admin</Link>}
```

Hiding a link is only for a nicer screen. It does **not** protect anything. A user can still type `/admin` in the address bar, so the route must also be guarded. And the server must check the token on every request.

## Logout

**Logout** means forgetting the user. In a JWT app, that means:

1. Remove the token from the app (set it to `null`).
2. Clear any user data in state.
3. Send the user to a public page, such as the home page.

```tsx
function handleLogout() {
  logout();          // token = null, so user = null
  navigate("/");     // leave the protected page
}
```

A JWT stays valid until its `exp` time, even after logout. So real apps keep tokens short-lived, and the server can also keep a list of tokens that were logged out.

## Authentication and Authorization Best Practices

- **Always check on the server.** Hiding links and guarding routes in React only helps the user. The server must check the token and the role on every request, because browser code can be changed.
- **Never trust the token in the browser.** The app reads the payload to show the user's name, but only the server can verify the signature.
- **Never put secrets in a JWT.** The payload can be read by anyone. No passwords, no card numbers.
- **Use short expiry times.** Check `exp`, and log the user out when the token has expired.
- **Store tokens carefully.** Many apps ask the server to keep the token in an **httpOnly cookie**, which JavaScript cannot read. A token in `localStorage` can be stolen if the site has a security hole that lets someone run their own script.
- **Always use HTTPS**, so the password and token cannot be read on the way.
- **Show general error messages**, like "Wrong email or password". Do not say which one was wrong.

React guards are for a good user experience. Real security happens on the server.

## Capstone Project: E-Commerce Access Management System

You will build a small shop where access depends on who you are. **Guests** can browse. **Customers** can register, log in and use a cart. **Admins** can manage product prices. It uses everything from this lesson: JWT, an auth context, route guards and roles.

<figure className="react-fig">
<svg viewBox="0 0 520 130" role="img" aria-labelledby="fig-uac-cap-flow" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-uac-cap-flow">E-commerce access flow: register or log in, get a JWT, keep it in the auth context, and let guards and roles decide what to show</title>
  <rect x="5" y="35" width="95" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="52.5" y="57.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Register</text>
  <text x="52.5" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>or Log in</text>
  <path d="M100 60 L120 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M120 60 L111.7 63.5 L111.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="125" y="35" width="85" height="50" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="167.5" y="57.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Server</text>
  <text x="167.5" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>checks</text>
  <path d="M210 60 L230 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M230 60 L221.7 63.5 L221.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="235" y="35" width="85" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="277.5" y="57.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>JWT</text>
  <text x="277.5" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>token</text>
  <path d="M320 60 L340 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M340 60 L331.7 63.5 L331.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="345" y="35" width="80" height="50" rx="8" style={{ fill: "var(--info-soft)", stroke: "var(--info)", strokeWidth: 1.5 }} />
  <text x="385.0" y="57.0" textAnchor="middle" style={{ fill: "var(--info)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Auth</text>
  <text x="385.0" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>context</text>
  <path d="M425 60 L440 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M440 60 L431.7 63.5 L431.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="445" y="35" width="70" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="480.0" y="57.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Guards</text>
  <text x="480.0" y="74.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>+ roles</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="3.2s" repeatCount="indefinite" path="M100 60 L120 60 M210 60 L230 60 M320 60 L340 60 M425 60 L440 60" />
  </circle>
  <text x="260" y="112" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 12, fontWeight: 400, fontFamily: "var(--sans)" }}>every page and link reads the user from the auth context</text>
</svg>
<figcaption>The token says who you are. The role says what you may do.</figcaption>
</figure>

### User Registration

`register(name, email, password)` in `authService.ts` checks the input (a name, a valid email, a password of 6+ characters, an email not used before), saves a new account with the role **customer**, and returns a token. New users never choose their own role: only the server gives out the admin role.

### User Login

`login(email, password)` finds the account and returns a token, or throws "Wrong email or password." The login form shows that message and stays on the page.

### JWT Authentication

Both functions return a **JWT**. Its payload holds the user's id, name, email, **role** and an expiry time (`exp`). The app reads the user from the token with `readUser(token)`. In this playground the signature is fake; a real server signs the token with a secret key and checks it on every request.

Try both in the lab. Register a new account, then look at the token and the user read from it.

      <form style={{ display: "grid", gap: 6 }} onSubmit={(e) => run(e, "register")}>
        <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
        <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
        <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
        <div>
          <button type="submit">Register</button>{" "}
          <button type="button" onClick={(e) => run(e, "login")}>Log in</button>
        </div>
      </form>
      {error && <p style={{ color: "crimson" }}>{error}</p>}
      {token && (
        <>
          <p><b>Token:</b></p>
          <code style={{ wordBreak: "break-all", fontSize: 11 }}>{token}</code>
          <p><b>User read from the token:</b></p>
          <pre>{JSON.stringify(readUser(token), null, 2)}</pre>
        </>
      )}
      <p style={{ color: "#777", fontSize: 12 }}>Try: register twice with the same email, or log in as admin@shop.com / admin123.</p>
    </div>
  );
}
`,
    "authService.ts": `export type Role = "customer" | "admin";

  id: string;
  name: string;
  email: string;
  role: Role;
  exp: number;
}

interface Account {
  id: string;
  name: string;
  email: string;
  password: string;
  role: Role;
}

// Pretend database. A real server stores hashed passwords, never plain ones.
const accounts: Account[] = [
  { id: "1", name: "Ravi", email: "admin@shop.com", password: "admin123", role: "admin" },
  { id: "2", name: "Asha", email: "asha@shop.com", password: "asha123", role: "customer" },
];

const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
const encode = (part: object) =>
  btoa(JSON.stringify(part)).replace(/=+$/, "").replace(/\\+/g, "-").replace(/\\//g, "_");

// The server signs a JWT. Here the signature is fake; a real server uses a secret key.
function makeToken(a: Account): string {
  const payload = { sub: a.id, name: a.name, email: a.email, role: a.role, exp: Math.floor(Date.now() / 1000) + 3600 };
  return encode({ alg: "HS256", typ: "JWT" }) + "." + encode(payload) + ".demo-signature";
}

  await wait(300);
  const cleanEmail = email.trim().toLowerCase();
  if (name.trim().length < 2) throw new Error("Please enter your name.");
  if (!cleanEmail.includes("@")) throw new Error("Please enter a valid email.");
  if (password.length < 6) throw new Error("The password needs at least 6 characters.");
  if (accounts.some((a) => a.email === cleanEmail)) throw new Error("This email is already registered.");
  const account: Account = { id: String(accounts.length + 1), name: name.trim(), email: cleanEmail, password, role: "customer" };
  accounts.push(account);
  console.log("[API] POST /register → 201 (new customer: " + account.email + ")");
  return makeToken(account);
}

  await wait(300);
  const account = accounts.find((a) => a.email === email.trim().toLowerCase() && a.password === password);
  console.log("[API] POST /login → " + (account ? 200 : 401));
  if (!account) throw new Error("Wrong email or password.");
  return makeToken(account);
}

// Read the user from the token's payload (the middle part)
  const p = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
  return { id: p.sub, name: p.name, email: p.email, role: p.role, exp: p.exp };
}

  return user.exp * 1000 < Date.now();
}

// RBAC: what each role may do
const permissions: Record<Role, string[]> = {
  customer: ["shop", "use-cart"],
  admin: ["shop", "manage-products"],
};

  return permissions[role].includes(action);
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### Role-Based Access Control (RBAC)

`permissions` in `authService.ts` lists what each role may do: a customer may `use-cart`, an admin may `manage-products`. `can(role, action)` answers yes or no. Pages and guards ask `can(...)`, so the rules live in **one** place.

### Protected Routes

Two guards in `Guards.tsx` protect the routes:

- `RequireAuth` sends guests to `/login`, and remembers the page they wanted, so they go back there after logging in.
- `RequirePermission action="manage-products"` sends users without that permission to `/no-access`.

So `/cart` is for customers only, and `/admin` is for admins only, even if someone types the URL by hand.

### Authentication State Management

`AuthContext.tsx` keeps the **token** in state and works out the `user` from it. It shares `user`, `login`, `register` and `logout` with every component. An expired token counts as logged out.

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-uac-cap-roles" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-uac-cap-roles">Each role sees different navigation links and pages</title>
  <rect x="10" y="15" width="150" height="40" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="85.0" y="40.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Guest</text>
  <rect x="185" y="15" width="150" height="40" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="260.0" y="40.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Customer</text>
  <rect x="360" y="15" width="150" height="40" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="435.0" y="40.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Admin</text>
  <rect x="10" y="65" width="150" height="90" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="85.0" y="115.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Shop · Log in · Register</text>
  <rect x="185" y="65" width="150" height="90" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="260.0" y="107.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Shop · 🛒 Cart</text>
  <text x="260.0" y="124.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>Add to cart</text>
  <rect x="360" y="65" width="150" height="90" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="435.0" y="107.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Shop · ⚙️ Admin</text>
  <text x="435.0" y="124.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>Edit prices</text>
  <rect x="10" y="15" width="150" height="140" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="4.5s" begin="0s" repeatCount="indefinite" />
  </rect>
  <rect x="185" y="15" width="150" height="140" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="4.5s" begin="1.5s" repeatCount="indefinite" />
  </rect>
  <rect x="360" y="15" width="150" height="140" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="4.5s" begin="3s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>Same app, three views. Guards also block the URLs, not only the links.</figcaption>
</figure>

### User Navigation

`NavBar` shows links for the current user. Guests see **Log in** and **Register**. Customers see **Cart** with the item count. Everyone logged in sees their name, role and a **Log out** button.

### Admin Navigation

Admins see an **Admin** link instead of the cart. The admin page lists products with a price box. Changing a price updates the shop at once.

### Cart Management

`StoreContext.tsx` holds the products and the cart (product id → quantity). It gives `addToCart`, `removeFromCart`, `cartCount` and `cartTotal`. The cart is emptied when the user logs out or changes, so one person's cart never shows for another.

### Conditional Rendering Based on Roles

The shop page shows a different action on each product card:

- Guest: **Log in to buy**
- Customer: **Add to cart**
- Admin: **Edit price** (a link to the admin page)

Here is the complete system. Try it as a guest, then log in as the customer (asha@shop.com / asha123), add items and open the cart. Log out and log in as the admin (admin@shop.com / admin123), change a price, then try to open the cart.

          <div className="shop">
              <Route path="/" element={<ShopPage />} />
              <Route path="/login" element={<LoginPage />} />
              <Route path="/register" element={<RegisterPage />} />
              <Route path="/cart" element={<RequireAuth><RequirePermission action="use-cart"><CartPage /></RequirePermission></RequireAuth>} />
              <Route path="/admin" element={<RequireAuth><RequirePermission action="manage-products"><AdminPage /></RequirePermission></RequireAuth>} />
              <Route path="/no-access" element={<NoAccessPage />} />
          </div>
  );
}
`,
    "authService.ts": `export type Role = "customer" | "admin";

  id: string;
  name: string;
  email: string;
  role: Role;
  exp: number;
}

interface Account {
  id: string;
  name: string;
  email: string;
  password: string;
  role: Role;
}

// Pretend database. A real server stores hashed passwords, never plain ones.
const accounts: Account[] = [
  { id: "1", name: "Ravi", email: "admin@shop.com", password: "admin123", role: "admin" },
  { id: "2", name: "Asha", email: "asha@shop.com", password: "asha123", role: "customer" },
];

const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
const encode = (part: object) =>
  btoa(JSON.stringify(part)).replace(/=+$/, "").replace(/\\+/g, "-").replace(/\\//g, "_");

// The server signs a JWT. Here the signature is fake; a real server uses a secret key.
function makeToken(a: Account): string {
  const payload = { sub: a.id, name: a.name, email: a.email, role: a.role, exp: Math.floor(Date.now() / 1000) + 3600 };
  return encode({ alg: "HS256", typ: "JWT" }) + "." + encode(payload) + ".demo-signature";
}

  await wait(300);
  const cleanEmail = email.trim().toLowerCase();
  if (name.trim().length < 2) throw new Error("Please enter your name.");
  if (!cleanEmail.includes("@")) throw new Error("Please enter a valid email.");
  if (password.length < 6) throw new Error("The password needs at least 6 characters.");
  if (accounts.some((a) => a.email === cleanEmail)) throw new Error("This email is already registered.");
  const account: Account = { id: String(accounts.length + 1), name: name.trim(), email: cleanEmail, password, role: "customer" };
  accounts.push(account);
  console.log("[API] POST /register → 201 (new customer: " + account.email + ")");
  return makeToken(account);
}

  await wait(300);
  const account = accounts.find((a) => a.email === email.trim().toLowerCase() && a.password === password);
  console.log("[API] POST /login → " + (account ? 200 : 401));
  if (!account) throw new Error("Wrong email or password.");
  return makeToken(account);
}

// Read the user from the token's payload (the middle part)
  const p = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
  return { id: p.sub, name: p.name, email: p.email, role: p.role, exp: p.exp };
}

  return user.exp * 1000 < Date.now();
}

// RBAC: what each role may do
const permissions: Record<Role, string[]> = {
  customer: ["shop", "use-cart"],
  admin: ["shop", "manage-products"],
};

  return permissions[role].includes(action);
}
`,
    "AuthContext.tsx": `import { createContext, useContext, useState, type ReactNode } from "react";

interface AuthValue {
  user: User | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  register: (name: string, email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthValue | null>(null);

  const [token, setToken] = useState<string | null>(null);

  // The user comes from the token. An expired token means "logged out".
  const decoded = token ? authService.readUser(token) : null;
  const user = decoded && !authService.isExpired(decoded) ? decoded : null;

  const value: AuthValue = {
    user,
    token,
    login: async (email, password) => setToken(await authService.login(email, password)),
    register: async (name, email, password) => setToken(await authService.register(name, email, password)),
    logout: () => setToken(null),
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

  const value = useContext(AuthContext);
  if (!value) throw new Error("useAuth must be used inside <AuthProvider>");
  return value;
}
`,
    "StoreContext.tsx": `import { createContext, useContext, useEffect, useState, type ReactNode } from "react";

interface StoreValue {
  products: Product[];
  updatePrice: (id: number, price: number) => void;
  cart: Record<number, number>; // product id -> quantity
  addToCart: (id: number) => void;
  removeFromCart: (id: number) => void;
  cartCount: number;
  cartTotal: number;
}

const StoreContext = createContext<StoreValue | null>(null);

  const { user } = useAuth();
  const [products, setProducts] = useState<Product[]>(startProducts);
  const [cart, setCart] = useState<Record<number, number>>({});

  // Each user has their own cart: empty it when the user changes or logs out
  useEffect(() => {
    setCart({});
  }, [user?.id]);

  const cartCount = Object.values(cart).reduce((sum: number, q) => sum + Number(q), 0);
  const cartTotal = Object.entries(cart).reduce(
    (sum: number, [id, q]) => sum + (products.find((p) => p.id === Number(id))?.price ?? 0) * Number(q),
    0,
  );

  const value: StoreValue = {
    products,
    updatePrice: (id, price) => setProducts((list) => list.map((p) => (p.id === id ? { ...p, price } : p))),
    cart,
    addToCart: (id) => setCart((c) => ({ ...c, [id]: (c[id] ?? 0) + 1 })),
    removeFromCart: (id) =>
      setCart((c) => {
        const next = { ...c };
        delete next[id];
        return next;
      }),
    cartCount,
    cartTotal,
  };

  return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}

  const value = useContext(StoreContext);
  if (!value) throw new Error("useStore must be used inside <StoreProvider>");
  return value;
}
`,
    "Guards.tsx": `import { Navigate, useLocation } from "react-router-dom";

// Logged-in users only. Others go to /login, and come back after logging in.
  const { user } = useAuth();
  const location = useLocation();
  if (!user) return <Navigate to="/login" replace state={{ from: location.pathname }} />;
  return <>{children}</>;
}

// Only roles that may do this action. Others see a "no access" page.
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  if (!can(user.role, action)) return <Navigate to="/no-access" replace />;
  return <>{children}</>;
}
`,
    "NavBar.tsx": `import { NavLink, useNavigate } from "react-router-dom";

// The links depend on who is logged in
  const { user, logout } = useAuth();
  const { cartCount } = useStore();
  const navigate = useNavigate();

  return (
    <nav className="nav">
      <NavLink to="/" end>Shop</NavLink>
      {!user && (
        <>
          <NavLink to="/login">Log in</NavLink>
          <NavLink to="/register">Register</NavLink>
        </>
      )}
      {user?.role === "customer" && <NavLink to="/cart">🛒 Cart ({cartCount})</NavLink>}
      {user?.role === "admin" && <NavLink to="/admin">⚙️ Admin</NavLink>}
      {user && (
        <span className="who">
          {user.name} ({user.role}){" "}
          <button onClick={() => { logout(); navigate("/"); }}>Log out</button>
        </span>
      )}
    </nav>
  );
}
`,
    "pages.tsx": `import { useState, type FormEvent } from "react";

  const { user } = useAuth();
  const { products, addToCart } = useStore();
  return (
    <section>
      <h2>Shop</h2>
      <div className="grid">
        {products.map((p) => (
          <div key={p.id} className="card">
            <b>{p.name}</b>
            <span>₹{p.price}</span>
            {!user && <Link to="/login">Log in to buy</Link>}
            {user?.role === "customer" && <button className="add" onClick={() => addToCart(p.id)}>Add to cart</button>}
            {user?.role === "admin" && <Link to="/admin">Edit price</Link>}
          </div>
        ))}
      </div>
    </section>
  );
}

  const { products, cart, removeFromCart, cartTotal } = useStore();
  const lines = Object.entries(cart).map(([id, qty]) => ({ product: products.find((p) => p.id === Number(id))!, qty: Number(qty) }));
  if (lines.length === 0) return <p>Your cart is empty. <Link to="/">Go shopping</Link></p>;
  return (
    <section>
      <h2>Your cart</h2>
      <ul className="lines">
        {lines.map(({ product, qty }) => (
          <li key={product.id}>
            {product.name} × {qty} = ₹{product.price * qty}{" "}
            <button onClick={() => removeFromCart(product.id)}>Remove</button>
          </li>
        ))}
      </ul>
      <p className="total">Total: ₹{cartTotal}</p>
    </section>
  );
}

  const { products, updatePrice } = useStore();
  return (
    <section>
      <h2>Admin: products</h2>
      <table className="table">
        <tbody>
          {products.map((p) => (
            <tr key={p.id}>
              <td>{p.name}</td>
              <td>
                ₹<input type="number" value={p.price} onChange={(e) => updatePrice(p.id, Number(e.target.value))} />
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <p className="muted">Price changes show at once in the shop.</p>
    </section>
  );
}

  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  const [email, setEmail] = useState("asha@shop.com");
  const [password, setPassword] = useState("asha123");
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);

  async function handleSubmit(e: FormEvent) {
    e.preventDefault();
    setError("");
    setBusy(true);
    try {
      await login(email, password);
      const from = (location.state as { from?: string } | null)?.from ?? "/";
      navigate(from, { replace: true });
    } catch (err) {
      setError((err as Error).message);
      setBusy(false);
    }
  }

  return (
    <form className="form" onSubmit={handleSubmit}>
      <h2>Log in</h2>
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
      {error && <p className="error">{error}</p>}
      <button type="submit" disabled={busy}>{busy ? "Checking..." : "Log in"}</button>
      <p className="muted">Customer: asha@shop.com / asha123 · Admin: admin@shop.com / admin123</p>
    </form>
  );
}

  const { register } = useAuth();
  const navigate = useNavigate();
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);

  async function handleSubmit(e: FormEvent) {
    e.preventDefault();
    setError("");
    setBusy(true);
    try {
      await register(name, email, password); // new accounts are customers
      navigate("/", { replace: true });
    } catch (err) {
      setError((err as Error).message);
      setBusy(false);
    }
  }

  return (
    <form className="form" onSubmit={handleSubmit}>
      <h2>Create an account</h2>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password (6+ characters)" />
      {error && <p className="error">{error}</p>}
      <button type="submit" disabled={busy}>{busy ? "Creating..." : "Register"}</button>
    </form>
  );
}

  return (
    <p className="error">
      Sorry, your role does not have access to this page. <Link to="/">Back to the shop</Link>
    </p>
  );
}
`,
    "products.ts": `export interface Product {
  id: number;
  name: string;
  price: number;
}

  { id: 1, name: "The Alchemist", price: 299 },
  { id: 2, name: "Wings of Fire", price: 350 },
  { id: 3, name: "Gel Pen Set", price: 120 },
  { id: 4, name: "Canvas Tote Bag", price: 399 },
];
`,
    "styles.css": `.shop { font-family: sans-serif; max-width: 560px; }
.nav { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; border-bottom: 2px solid #2b59c3; padding-bottom: 6px; margin-bottom: 10px; }
.nav a { color: #2b59c3; text-decoration: none; padding: 3px 8px; border-radius: 6px; }
.nav a.active { background: #2b59c3; color: white; }
.who { margin-left: auto; font-size: 13px; color: #444; }
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.card { display: grid; gap: 4px; border: 1px solid #ddd; border-radius: 8px; padding: 8px; }
.form { display: grid; gap: 8px; max-width: 300px; }
.form input { padding: 6px; }
.error { color: #b42318; margin: 0; }
.muted { color: #777; font-size: 12px; }
.lines { padding-left: 18px; }
.total { font-weight: bold; }
.table td { padding: 4px 8px; }
.table input { width: 80px; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Hiding a link is not security. The real protection is on the server: it must check the JWT and the role on **every** API request, for example "only admins may change prices". The downloadable project below does exactly that.

### Download Project Files

#### JWT Authentication Project Source Code

The complete shop is ready as a real project: a **Vite + React + TypeScript** front end and a small **Express** API that signs real JWTs with `jsonwebtoken`, stores hashed passwords with `bcryptjs`, and checks the token and role on every protected request.

Download JWT Authentication Project: [Download E-Commerce Access Management Project](/downloads/react/jwt-ecommerce-access.zip)

```
ecommerce-access/
  package.json             scripts for the web app and the API
  .env.example             JWT_SECRET and token lifetime
  server/
    index.js               Express API: register, login, products, cart, admin
    auth.js                sign and verify JWTs, requireAuth and requireRole middleware
    db.js                  JSON file storage (users, products, carts)
    data/                  users.json, products.json, carts.json
  src/
    main.tsx, App.tsx      providers and routes
    api/client.ts          fetch helper that adds "Authorization: Bearer <token>"
    context/               AuthContext (token + user), CartContext
    auth/                  permissions (RBAC), ProtectedRoute, RoleRoute
    components/            NavBar, ProductCard
    pages/                 Shop, Login, Register, Cart, Admin, NoAccess, NotFound
```

#### Project Setup Instructions

1. Install **Node.js** 18 or newer from nodejs.org, if you do not have it.
2. Download the ZIP with the link above, and unzip it. You get a folder called `ecommerce-access`.
3. Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
4. Install the packages and start both the API and the web app:

```bash
npm install
npm run dev
```

Open `http://localhost:5173`. Log in as **asha@shop.com / asha123** (customer) or **admin@shop.com / admin123** (admin), or register a new customer. The API runs at `http://localhost:3001`. Press `Ctrl + C` to stop both.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Hiding the Admin link but not guarding `/admin` | Anyone who types `/admin` sees the page | Wrap the route in a guard, and check on the server |
| Putting a password in the JWT payload | Anyone with the token can read the password | Only put non-secret details like id, name and role |
| Not checking `exp` | Users stay "logged in" with an expired token | Treat an expired token as logged out |
| Forgetting `replace` on the login redirect | Back button returns to the blocked page, and redirects again | Use `<Navigate to="/login" replace />` |
| Checking user names instead of roles | Code breaks when users change | Check roles or permissions with a `can()` function |
| Using `useAuth` outside `AuthProvider` | Error: useAuth must be used inside AuthProvider | Wrap the app in `<AuthProvider>` |
| Not sending the user back after login | They land on the home page and must find their page again | Save `from` in the redirect, and navigate there after login |

</div>

## Check Yourself

## Exercises

## What to Remember

- Authentication checks **who** the user is. Authorization checks **what** they may do. Authentication comes first.
- After login, the server gives a JWT. The app keeps it, reads the user from its payload, and sends it with each request.
- Keep the user in an auth context, protect routes with guards, check roles with a `can()` function, and always check again on the server.

Next: Client-Side Rendering
