diff --git a/_docs/02_document/components/11_class-colors/description.md b/_docs/02_document/components/11_class-colors/description.md index 9747acb..380376c 100644 --- a/_docs/02_document/components/11_class-colors/description.md +++ b/_docs/02_document/components/11_class-colors/description.md @@ -81,5 +81,5 @@ This *is* the helper. There are no further extensions inside this component. | Path | Module Doc | |------|------------| -| `src/class-colors/classColors.ts` | `_docs/02_document/modules/src__features__annotations__classColors.md` (path-derived name kept; content reflects the new home) | +| `src/class-colors/classColors.ts` | `_docs/02_document/modules/src__class-colors__classColors.md` | | `src/class-colors/index.ts` | barrel — re-exports `getClassColor`, `getClassNameFallback`, `getPhotoModeSuffix`, `FALLBACK_CLASS_NAMES` | diff --git a/_docs/02_document/modules/src__api__endpoints.md b/_docs/02_document/modules/src__api__endpoints.md index 13699b2..59396d9 100644 --- a/_docs/02_document/modules/src__api__endpoints.md +++ b/_docs/02_document/modules/src__api__endpoints.md @@ -20,6 +20,7 @@ export const endpoints = { authLogout: () => string users: () => string user: (id: string) => string + usersMe: () => string // added 2026-05-13 by AZ-510 — chained read after POST refresh classes: () => string class: (id: string | number) => string }, @@ -81,7 +82,7 @@ The whole object is `as const`, so each leaf's return type is the narrow string After the AZ-486 migration, `endpoints` is imported by: - `src/api/client.ts` — internal `refreshToken()` helper uses `endpoints.admin.authRefresh()`. -- `src/auth/AuthContext.tsx` — `authRefresh`, `authLogin`, `authLogout`. +- `src/auth/AuthContext.tsx` — `authRefresh`, `authLogin`, `authLogout`, `usersMe` (added by AZ-510). - `src/components/FlightContext.tsx` — `flights.collection`, `flights.flight`, `annotations.settingsUser`. - `src/components/DetectionClasses.tsx` — `admin.classes`, `admin.class`. - `src/features/admin/AdminPage.tsx` — `admin.users`, `admin.user`. diff --git a/_docs/02_document/modules/src__auth__AuthContext.md b/_docs/02_document/modules/src__auth__AuthContext.md index d9a0e95..b9c771c 100644 --- a/_docs/02_document/modules/src__auth__AuthContext.md +++ b/_docs/02_document/modules/src__auth__AuthContext.md @@ -1,7 +1,8 @@ # Module: `src/auth/AuthContext.tsx` -> **Source**: `src/auth/AuthContext.tsx` (54 lines) +> **Source**: `src/auth/AuthContext.tsx` (~120 lines after AZ-510) > **Topo batch**: B3 (depends on B2 leaves: `api/client`, `types/index`) +> **Last refresh**: 2026-05-13 — AZ-510 consolidated bootstrap onto POST refresh + chained `/users/me`; closes Vision P3 / Finding B3. ## Purpose @@ -31,16 +32,30 @@ 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)**: +**Bootstrap effect (mount-only)** — AZ-510 wire shape: ```ts -api.get<{ user: AuthUser; token: string }>(endpoints.admin.authRefresh()) - .then(data => { setToken(data.token); setUser(data.user) }) - .catch(() => {}) - .finally(() => setLoading(false)) +async function runBootstrap(): Promise { + const refreshRes = await fetch(getApiBase() + endpoints.admin.authRefresh(), { + method: 'POST', + credentials: 'include', + }) + if (!refreshRes.ok) return null + const refreshData = (await refreshRes.json()) as { token: string } + setToken(refreshData.token) + try { + return await api.get(endpoints.admin.usersMe()) + } catch (err) { + console.error('[AuthContext] Refresh succeeded but /users/me failed:', err) + setToken(null) + return null + } +} ``` -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. +A module-scoped `bootstrapInflight: Promise | null` guard is consulted before invoking `runBootstrap`, so two concurrent `useEffect` mounts (React 18+ StrictMode dev double-mount, or rapid re-mount in tests) share a single network round-trip and avoid racing the backend's refresh-cookie rotation. A test-only escape hatch `__resetBootstrapInflightForTests()` is exported via the `src/auth` barrel and called in `tests/setup.ts`'s `afterEach` to keep the module-scoped promise from leaking between tests. + +The bootstrap and the existing 401-retry path in `api/client.ts:73` now share a single wire shape — both POST `/api/admin/auth/refresh` with `credentials:'include'` and rely on the HttpOnly refresh cookie. The chained `GET /api/admin/users/me` request fetches the user payload (the POST refresh response is `{ token }` only). On any failure path (refresh 401, refresh network error, refresh 200 → `/users/me` 401, refresh 200 → `/users/me` network error) the bootstrap clears the bearer first then sets `user: null` + `loading: false`, so an in-flight re-render never sees `(user: null, accessToken: )`. Closes Vision principle P3 ("bearer in memory, refresh in HttpOnly cookie") and Finding B3. **`login(email, password)`**: @@ -60,7 +75,7 @@ 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. +**`hasPermission(perm)`**: returns `user?.permissions?.includes(perm) ?? false`. Defensively handles legacy `/users/me` payloads that omit `permissions` (older backend builds; some test fixtures returning the bare `User` shape). Permission strings are not constrained at the type level — any string passes. Backend-defined; UI uses this only for affordance show/hide, never for security gates (the server is the authority — see `_docs/02_document/architecture.md` Vision P12 / O4). ## Dependencies @@ -103,14 +118,11 @@ No env vars consumed directly — token storage policy is defined in `client.ts` ## Tests -None. +`src/auth/AuthContext.test.tsx` — un-quarantined `FT-P-01` (bootstrap POST + `credentials:'include'` + chained `/users/me` regression guard); `FT-P-03` (refresh transparency, child re-render delta ≤ 1); `NFT-SEC-01` (bearer never in localStorage / sessionStorage across the full bootstrap + 401-retry lifecycle); `NFT-SEC-02` (no refresh-prefixed cookie visible via `document.cookie`); `AC-4 (AZ-510)` — POST refresh 200 → `/users/me` 401 clears the bearer + logs a diagnostic console.error. ## 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`). +- ~~**Bootstrap-vs-refresh divergence**~~ — **RESOLVED 2026-05-13 by AZ-510**. Bootstrap now uses POST + `credentials:'include'` + chained `/users/me`, sharing the same wire shape as the 401-retry path. `api.get()` is intentionally NOT used for the refresh itself because it does not thread `credentials:'include'`; the bootstrap calls `fetch()` directly with the same explicit-credentials pattern documented in `api/client.ts:88`. Finding B3 closed. - **`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. diff --git a/_docs/02_document/modules/src__features__annotations__classColors.md b/_docs/02_document/modules/src__class-colors__classColors.md similarity index 91% rename from _docs/02_document/modules/src__features__annotations__classColors.md rename to _docs/02_document/modules/src__class-colors__classColors.md index ff4fab0..2cb6bfe 100644 --- a/_docs/02_document/modules/src__features__annotations__classColors.md +++ b/_docs/02_document/modules/src__class-colors__classColors.md @@ -1,6 +1,7 @@ -# Module: `src/features/annotations/classColors.ts` +# Module: `src/class-colors/classColors.ts` -> **Source**: `src/features/annotations/classColors.ts` (24 lines) +> **Source**: `src/class-colors/classColors.ts` (24 lines; moved from `src/features/annotations/classColors.ts` by AZ-511 on 2026-05-13 — closes Finding F3) +> **Public API barrel**: `src/class-colors/index.ts` re-exports `getClassColor`, `getClassNameFallback`, `getPhotoModeSuffix`, `FALLBACK_CLASS_NAMES`. > **Topo batch**: B1 (leaf — no internal imports) ## Purpose diff --git a/_docs/02_document/modules/src__components__DetectionClasses.md b/_docs/02_document/modules/src__components__DetectionClasses.md index 325a2d7..9252c6d 100644 --- a/_docs/02_document/modules/src__components__DetectionClasses.md +++ b/_docs/02_document/modules/src__components__DetectionClasses.md @@ -1,7 +1,8 @@ # Module: `src/components/DetectionClasses.tsx` > **Source**: `src/components/DetectionClasses.tsx` (99 lines) -> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `features/annotations/classColors`, `types/index`) +> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `class-colors` (via barrel), `types/index`) +> **Last refresh**: 2026-05-13 — `getClassColor` + `FALLBACK_CLASS_NAMES` import migrated from `'../features/annotations/classColors'` to `'../class-colors'` barrel by AZ-511. ## Purpose diff --git a/_docs/02_document/modules/src__features__annotations.md b/_docs/02_document/modules/src__features__annotations.md index f22b998..621d534 100644 --- a/_docs/02_document/modules/src__features__annotations.md +++ b/_docs/02_document/modules/src__features__annotations.md @@ -1,6 +1,6 @@ # Module group: `src/features/annotations/` -> Compact doc covering all 5 annotations modules (`classColors.ts` is a shared leaf — see existing `src__features__annotations__classColors.md`). The annotations feature is the **central legacy concern** of the codebase per `_docs/legacy/wpf-era.md §4` (`Azaion.Annotator` window) — what's documented here is the React port. For the canonical product spec see `_docs/ui_design/README.md` (Annotations Tab Layout, Annotation Quality Guidelines, Affiliation Icons, Combat Readiness, Annotation Row Gradient, Keyboard Shortcuts, Video Annotation Time-Window Display) and parent suite `../../../../_docs/01_annotations.md` for the API contract. +> Compact doc covering the 4 annotations-feature modules. `classColors.ts` was carved out of this directory to its own component (`src/class-colors/`) by AZ-511 on 2026-05-13 — see `src__class-colors__classColors.md`; consumers in this feature now import via the `../../class-colors` barrel. The annotations feature is the **central legacy concern** of the codebase per `_docs/legacy/wpf-era.md §4` (`Azaion.Annotator` window) — what's documented here is the React port. For the canonical product spec see `_docs/ui_design/README.md` (Annotations Tab Layout, Annotation Quality Guidelines, Affiliation Icons, Combat Readiness, Annotation Row Gradient, Keyboard Shortcuts, Video Annotation Time-Window Display) and parent suite `../../../../_docs/01_annotations.md` for the API contract. ## Scope @@ -20,7 +20,7 @@ Owns the `/annotations` route. Lets the user: | Module | Layer | Responsibility | |---|---|---| -| `classColors.ts` | leaf | (already documented separately) Class-number → colour + photoMode-suffix lookup. | +| ~~`classColors.ts`~~ | (moved) | Carved out by AZ-511 to `src/class-colors/`; imported via the `class-colors` barrel by `CanvasEditor.tsx`, `AnnotationsSidebar.tsx`, `AnnotationsPage.tsx`. | | `MediaList.tsx` | sub-component | Left panel media browser. Owns `media[]` state, debounced filter, dropzone upload, blob: local-mode fallback when backend POST fails. Calls `endpoints.annotations.media(qs)`, `endpoints.annotations.mediaItem(id)` (DELETE), `endpoints.annotations.mediaBatch()` (POST). | | `VideoPlayer.tsx` | sub-component | Native `