Files
ui/_docs/02_document/modules/src__auth__AuthContext.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
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>
2026-05-11 00:38:49 +03:00

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 | nullnull when unauthenticated.
  • loading: booleantrue until 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/clientapi, setToken.
    • ../typesAuthUser 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 (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.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.