# Module: `src/auth/AuthContext.tsx`

> **Source**: `src/auth/AuthContext.tsx` (54 lines)
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `types/index`)

## Purpose

The single source of truth for the SPA's authentication state. Wraps the bearer-token plumbing from `api/client.ts` in a React context, exposes `useAuth()` for any descendant component, and bootstraps the session on app start by attempting a refresh. Together with `ProtectedRoute.tsx` and `LoginPage.tsx`, this is the WPF-era `LoginWindow.xaml` + auth service replacement (`_docs/legacy/wpf-era.md` §3 / §4).

## Public interface

```ts
interface AuthState {
  user: AuthUser | null
  loading: boolean
  login: (email: string, password: string) => Promise<void>
  logout: () => Promise<void>
  hasPermission: (perm: string) => boolean
}

export function useAuth(): AuthState
export function AuthProvider({ children }: { children: ReactNode }): JSX.Element
```

`AuthContext` itself is module-private (`createContext<AuthState>(null!)`). Consumers must go through `useAuth()`.

## Internal logic

State:

- `user: AuthUser | null` — `null` when unauthenticated.
- `loading: boolean` — `true` until the initial refresh attempt resolves (success or failure). Renders should gate on this.

**Bootstrap effect (mount-only)**:

```ts
api.get<{ user: AuthUser; token: string }>(endpoints.admin.authRefresh())
  .then(data => { setToken(data.token); setUser(data.user) })
  .catch(() => {})
  .finally(() => setLoading(false))
```

The refresh endpoint is invoked with `credentials: 'include'` only inside `client.ts`'s **internal** `refreshToken()` helper — but here we go through the public `api.get()` path, which does NOT include credentials. **This is a real divergence**: `client.ts`'s internal `refreshToken()` (used in the 401 retry) sends the cookie; the bootstrap call in `AuthContext` does not. The endpoint must therefore accept the refresh either via cookie (then bootstrap fails silently for non-cookie clients — which is everyone after a hard reload) **or** via some other mechanism (a refresh token in `localStorage`, etc.). **Flag for Step 4 verification** against the `admin/` service contract; this is likely a real bug masking by silent `.catch`. The path string itself is unaffected by AZ-486 — `endpoints.admin.authRefresh()` produces `'/api/admin/auth/refresh'` character-identically to the pre-refactor literal, so the divergence is structural, not URL-based.

**`login(email, password)`**:

```ts
const data = await api.post<{ token; user }>(endpoints.admin.authLogin(), { email, password })
setToken(data.token); setUser(data.user)
```

Throws to caller (LoginPage) on bad credentials.

**`logout()`**:

```ts
try { await api.post(endpoints.admin.authLogout()) } catch {}
setToken(null); setUser(null)
```

Network failure on logout is silently swallowed because we want to clear local auth state regardless.

**`hasPermission(perm)`**: returns `user?.permissions.includes(perm) ?? false`. The permission strings are not constrained at the type level — any string passes. Backend-defined.

## Dependencies

- **Internal**:
  - `../api` (barrel) — `api`, `endpoints`, `setToken`. (Since AZ-485 / F4 + AZ-486 / F7: barrel import + endpoint builders.)
  - `../types` — `AuthUser` type.
- **External**: `react` (`createContext`, `useContext`, `useState`, `useCallback`, `useEffect`, `ReactNode`).

## Consumers (intra-repo)

From the §7a dependency graph:

- `src/auth/ProtectedRoute.tsx` — gates routed children on `user !== null`.
- `src/components/Header.tsx` — shows current user, exposes Logout.
- `src/features/login/LoginPage.tsx` — calls `login(...)`, redirects on success.
- `src/App.tsx` — mounts `AuthProvider` near the root.

(Other features rely on the bearer token implicitly via `api/client.ts` — they don't import `useAuth` directly.)

## Data models

`AuthUser` (from `src/types/index.ts`) — see `_docs/02_document/modules/src__types__index.md`. Carries at minimum `id`, `email`, `permissions: string[]`.

## Configuration

Endpoints (typed builders from `../api/endpoints`, since AZ-486 / F7): `endpoints.admin.authRefresh()`, `endpoints.admin.authLogin()`, `endpoints.admin.authLogout()` — producing `/api/admin/auth/refresh`, `.../login`, `.../logout` respectively. Routed by `nginx.conf` to the `admin/` service.

No env vars consumed directly — token storage policy is defined in `client.ts` (in-memory; not persisted to `localStorage`).

## External integrations

`admin/` (.NET) auth service via `/api/admin/auth/{refresh,login,logout}`.

## Security

- **In-memory token only**: the bearer is held by `client.ts` at module scope. Survives intra-tab navigation, lost on hard reload — at which point the refresh path must restore it (currently broken per the bootstrap-effect note above).
- **`hasPermission` runs client-side only**: the server is the authority; `hasPermission` is for UI affordances (hide vs. show buttons). The backend MUST re-check permissions on every endpoint. Document in `security_approach.md` (Step 6).
- **Silent error swallowing on bootstrap and logout** is intentional but obscures real failures. A dev-only `console.error` would help during the testability pass; do not add a user-visible toast (silent recovery is the correct UX).
- **No XSS exfiltration risk for the bearer**: in-memory only, never written to `localStorage` or a non-HttpOnly cookie. (Confirmed in `client.ts` doc.)

## Tests

None.

## Notes / open questions

- **Bootstrap-vs-refresh divergence** (above) — the highest-priority flag in this module. Either:
  1. The refresh endpoint accepts an Authorization-less, cookie-bearing call → confirm the `admin/` service sets an HttpOnly cookie on `/login` and the cookie path matches `/api/admin/auth/refresh`. The `api.get()` path in `client.ts` does NOT send `credentials: 'include'`, so this currently CANNOT work. → **likely bug**.
  2. Or the bootstrap should be calling the internal `refreshToken()` helper, which is currently not exported.
  Either way, this needs a Step 4 fix (export `refreshToken()` and call it here, or change `api.get()` to allow per-call `credentials`).
- **`AuthContext = createContext<AuthState>(null!)`**: the non-null assertion means `useAuth()` will throw at the destructuring site if it's used outside `AuthProvider`. Acceptable given `App.tsx` mounts `AuthProvider` at the top, but a guard `if (!ctx) throw new Error(...)` would be friendlier. Defer.
- The `loading` flag is never re-set to `true` after the initial bootstrap. `login` and `logout` complete synchronously from the React tree's perspective (the `await` is inside the callback). If a future requirement demands a "logging in…" indicator, it would need its own state. Note for Step 8.
- `useAuth` returns the raw context value (no memoisation wrapper). React 18+ behaviour means `<AuthProvider>` re-renders all `useAuth` consumers on every state update — fine here because there's no high-frequency state.
