mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 10:41:10 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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 }>('/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)`**:
|
||||
|
||||
```ts
|
||||
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()`**:
|
||||
|
||||
```ts
|
||||
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` — `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 (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.
|
||||
Reference in New Issue
Block a user