Captures the full output of autodev existing-code Phase A through Step 4 (Code Testability Revision) for the Azaion UI workspace: - Step 1 Document: _docs/02_document/ (FINAL_report, architecture, glossary, components/, modules/, diagrams/, system-flows, module-layout) plus _docs/00_problem/ + _docs/01_solution/ + _docs/legacy/ + _docs/how_to_test + README. - Step 2 Architecture Baseline: architecture_compliance_baseline.md. - Step 3 Test Spec: _docs/02_document/tests/ (environment, test-data, blackbox/performance/resilience/security/ resource-limit tests, traceability-matrix), enum_spec_snapshot, expected_results/results_report.md (98 rows), plus the run-tests.sh + run-performance-tests.sh runners. - Step 4 Code Testability Revision: 01-testability-refactoring/ run dir (list-of-changes C01-C07, deferred_to_refactor, analysis/research_findings + refactoring_roadmap) and the 7 child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/ plus _dependencies_table.md. - _docs/_autodev_state.md pins the cursor at Step 4 / refactor Phase 4 entry so /autodev resumes cleanly. Epic AZ-447 (UI testability gates) tracks the 7 child tasks that will land in subsequent commits. Co-authored-by: Cursor <cursoragent@cursor.com>
6.4 KiB
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
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—nullwhen unauthenticated.loading: boolean—trueuntil the initial refresh attempt resolves (success or failure). Renders should gate on this.
Bootstrap effect (mount-only):
api.get<{ user: AuthUser; token: string }>('/api/admin/auth/refresh')
.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.
login(email, password):
const data = await api.post<{ token; user }>('/api/admin/auth/login', { email, password })
setToken(data.token); setUser(data.user)
Throws to caller (LoginPage) on bad credentials.
logout():
try { await api.post('/api/admin/auth/logout') } 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/client—api,setToken.../types—AuthUsertype.
- External:
react(createContext,useContext,useState,useCallback,useEffect,ReactNode).
Consumers (intra-repo)
From the §7a dependency graph:
src/auth/ProtectedRoute.tsx— gates routed children onuser !== null.src/components/Header.tsx— shows current user, exposes Logout.src/features/login/LoginPage.tsx— callslogin(...), redirects on success.src/App.tsx— mountsAuthProvidernear 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 (string-literal): /api/admin/auth/refresh, /api/admin/auth/login, /api/admin/auth/logout. 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.tsat 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). hasPermissionruns client-side only: the server is the authority;hasPermissionis for UI affordances (hide vs. show buttons). The backend MUST re-check permissions on every endpoint. Document insecurity_approach.md(Step 6).- Silent error swallowing on bootstrap and logout is intentional but obscures real failures. A dev-only
console.errorwould 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
localStorageor a non-HttpOnly cookie. (Confirmed inclient.tsdoc.)
Tests
None.
Notes / open questions
- Bootstrap-vs-refresh divergence (above) — the highest-priority flag in this module. Either:
- The refresh endpoint accepts an Authorization-less, cookie-bearing call → confirm the
admin/service sets an HttpOnly cookie on/loginand the cookie path matches/api/admin/auth/refresh. Theapi.get()path inclient.tsdoes NOT sendcredentials: 'include', so this currently CANNOT work. → likely bug. - Or the bootstrap should be calling the internal
refreshToken()helper, which is currently not exported. Either way, this needs a Step 4 fix (exportrefreshToken()and call it here, or changeapi.get()to allow per-callcredentials).
- The refresh endpoint accepts an Authorization-less, cookie-bearing call → confirm the
AuthContext = createContext<AuthState>(null!): the non-null assertion meansuseAuth()will throw at the destructuring site if it's used outsideAuthProvider. Acceptable givenApp.tsxmountsAuthProviderat the top, but a guardif (!ctx) throw new Error(...)would be friendlier. Defer.- The
loadingflag is never re-set totrueafter the initial bootstrap.loginandlogoutcomplete synchronously from the React tree's perspective (theawaitis inside the callback). If a future requirement demands a "logging in…" indicator, it would need its own state. Note for Step 8. useAuthreturns the raw context value (no memoisation wrapper). React 18+ behaviour means<AuthProvider>re-renders alluseAuthconsumers on every state update — fine here because there's no high-frequency state.