# 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 logout: () => Promise hasPermission: (perm: string) => boolean } export function useAuth(): AuthState export function AuthProvider({ children }: { children: ReactNode }): JSX.Element ``` `AuthContext` itself is module-private (`createContext(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(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 `` re-renders all `useAuth` consumers on every state update — fine here because there's no high-frequency state.