Files
ui/_docs/02_document/components/02_auth/description.md
Oleksandr Bezdieniezhnykh 70fb452805 [AZ-510] Auth bootstrap: POST refresh + chained /users/me
Replace the broken `GET /api/admin/auth/refresh` (no `credentials:'include'`)
mount-time bootstrap with `POST /api/admin/auth/refresh` (with credentials)
chained to `GET /api/admin/users/me`. Returning users with a valid HttpOnly
refresh cookie no longer flash through `/login`. Closes Finding B3 / Vision P3.

- Add module-scoped `bootstrapInflight` guard (StrictMode double-mount safety)
  + test-only reset hook exported via the `src/auth` barrel; `tests/setup.ts`
  resets it in `afterEach` to prevent pending-promise leakage between tests.
- Defensive `hasPermission` against legacy `/users/me` payloads omitting
  `permissions`; default MSW handler now seeds `permissions` explicitly.
- Add `endpoints.admin.usersMe()` builder (STC-ARCH-02 forbids the literal).
- Bulk-swap 15 test files from `http.get` -> `http.post` for the refresh
  override so intentional bootstrap-fail tests still fail correctly.
- Update auth component description; mark B3 closed.
- Code review verdict PASS; static + fast suites green (231 / 13 skipped).

Batch report: _docs/03_implementation/batch_13_cycle3_report.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 02:59:31 +03:00

4.9 KiB

02 — Auth

1. High-Level Overview

Purpose: Authentication state, login/logout/refresh plumbing, and the <ProtectedRoute> gate that wraps every non-public route.

Architectural Pattern: React Context provider + route-guard component.

Upstream dependencies: 00_foundation (types), 01_api-transport.

Downstream consumers: 04_login, 10_app-shell, every authenticated page (indirectly via the route gate).

2. Internal Interfaces

src/auth/AuthContext.tsx

Export Signature Notes
AuthProvider({ children }) React component Wraps the app below BrowserRouter. Bootstraps via POST /api/admin/auth/refresh (with credentials: 'include') chained with GET /api/admin/users/me on mount — same wire shape as the 401-retry path in api/client.ts.
useAuth(): AuthContextValue hook Read-only access to { user, permissions, login, logout, refresh, loading }.

AuthContextValue (output DTO):

user:            User | null
permissions:     Permission[]   ← from server, used by route guards & UI
loading:         boolean        ← true during initial bootstrap and active refresh
login(c):        Promise<User>  ← POST /api/admin/auth/login
logout():        Promise<void>  ← POST /api/admin/auth/logout
refresh():       Promise<void>  ← POST /api/admin/auth/refresh

src/auth/ProtectedRoute.tsx

Export Signature Notes
ProtectedRoute({ children, requirePermission? }) React component Renders children if authenticated; otherwise navigates to /login. Optional requirePermission is checked against useAuth().permissions and renders a 403 placeholder on miss.

3. External API Specification

Consumes only — does not expose. Endpoint set (from _docs/02_document/modules/src__auth__AuthContext.md):

Method Path Auth When
POST /api/admin/auth/login public Login form submit
POST /api/admin/auth/refresh cookie Bootstrap + on 401 retry
POST /api/admin/auth/logout cookie Header → Logout
GET /api/admin/auth/me cookie (post-login profile fetch, if implemented)

5. Implementation Details

State Management: Single React context. Token lives in an HTTP-only cookie (server-managed); the React state holds only the parsed user + permissions. No localStorage.

Bootstrap sequence (consolidated by AZ-510):

  1. Mount → set loading: true.
  2. fetch(getApiBase() + endpoints.admin.authRefresh(), { method: 'POST', credentials: 'include' }) to ask the server "do I have a valid session?". Direct fetch (not api.post) because api.post does not thread credentials: 'include' and widening it would change CORS posture for every authed callsite.
  3. On 200 → setToken(data.token), then api.get(endpoints.admin.usersMe()) to fetch the user shape (the POST refresh response is { token } only — no user payload). On /users/me 200 → setUser(authUser), loading: false. On /users/me failure → setToken(null), setUser(null), loading: false, console.error carries the diagnostic (refresh OK / user GET failed).
  4. On refresh 4xx or network failure → setUser(null), loading: false. ProtectedRoute then redirects to /login.
  5. StrictMode: a module-scoped in-flight promise deduplicates the bootstrap network round-trip across React 18+ StrictMode double-mounts so the backend cookie rotation does not race itself.

Bootstrap and the 401-retry path in api/client.ts:88 now share a single wire shape — POST /api/admin/auth/refresh with credentials. Finding B3 (bootstrap missing credentials: 'include') is closed.

Spinner UX: ProtectedRoute renders a centered spinner during loading. The spinner has no role="status" / no accessible label / no timeout. (Findings B4, joint with Step 4 client.ts timeout flag.)

7. Caveats & Edge Cases

  • Bootstrap missing credentials: 'include' — closed by AZ-510. Bootstrap now uses POST refresh + chained /users/me with credentials, matching the 401-retry path.
  • Spinner accessibility — Step 4.
  • Token-rotation interaction with SSE — see 01_api-transport. Auth refresh works for fetch but breaks every active EventSource.
  • No idle-timeout / inactivity logout — server-side concern; UI tolerates whatever the server enforces.

8. Dependency Graph

Must be implemented after: 00_foundation, 01_api-transport.

Can be implemented in parallel with: nothing inside this workspace's gate path.

Blocks: 03_shared-ui (Header reads useAuth), 04_login, 10_app-shell, indirectly every authenticated page.

Module Inventory

Path Module Doc
src/auth/AuthContext.tsx _docs/02_document/modules/src__auth__AuthContext.md
src/auth/ProtectedRoute.tsx _docs/02_document/modules/src__auth__ProtectedRoute.md