mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:01:10 +00:00
[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>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|
||||
| Export | Signature | Notes |
|
||||
|--------|-----------|-------|
|
||||
| `AuthProvider({ children })` | React component | Wraps the app below `BrowserRouter`. Bootstraps via `GET /api/admin/auth/refresh` on mount. |
|
||||
| `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):
|
||||
@@ -51,19 +51,20 @@ Consumes only — does not expose. Endpoint set (from `_docs/02_document/modules
|
||||
|
||||
**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**:
|
||||
**Bootstrap sequence** (consolidated by AZ-510):
|
||||
1. Mount → set `loading: true`.
|
||||
2. `api.post('/api/admin/auth/refresh')` to ask the server "do I have a valid session?".
|
||||
3. On 200 → store user + permissions, `loading: false`.
|
||||
4. On 4xx → user stays `null`, `loading: false`. `ProtectedRoute` then redirects.
|
||||
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.
|
||||
|
||||
> **PRIORITY finding (B3, copied from state.json)**: the bootstrap call inside `AuthContext.tsx` does not pass `credentials: 'include'` consistently — the cookie is therefore not sent on the very first request and bootstrap silently fails on a fresh page load. Confirmed real bug; Step 4 fix.
|
||||
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'`** → users land on `/login` even with a valid cookie session. PRIORITY Step 4.
|
||||
- ~~**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.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Batch 13 — AZ-510 (Auth bootstrap refresh consolidation)
|
||||
|
||||
**Date**: 2026-05-13
|
||||
**Cycle**: 3 — autodev Step 10 (Implement), batch 1 of 3 (fixes-first order: AZ-510 → AZ-511 → AZ-512)
|
||||
**Tickets**: AZ-510 (Epic AZ-509)
|
||||
**Verdict**: PASS
|
||||
|
||||
---
|
||||
|
||||
## Task Results
|
||||
|
||||
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|
||||
|------|--------|----------------|-------|-------------|--------|
|
||||
| AZ-510_auth_bootstrap_consolidation | Done | 25 files | 231 passed / 13 skipped (full fast suite) | 6/6 ACs covered | None |
|
||||
|
||||
## AC Test Coverage: 6/6 covered
|
||||
|
||||
- AC-1 → `AuthContext.test.tsx` FT-P-01 (POST + `credentials:'include'` + no GET refresh)
|
||||
- AC-2 → FT-P-01 (chain to `/users/me`, bearer set, loading false)
|
||||
- AC-3 → `ProtectedRoute.test.tsx` (failed bootstrap → spinner → `/login` once); also
|
||||
exercised by NFT-SEC-01's intermediate state
|
||||
- AC-4 → `AuthContext.test.tsx` "AC-4 (AZ-510)" test (new, lines 108-138)
|
||||
- AC-5 → `ProtectedRoute.test.tsx` admin-route success cases (no `/login` on success bootstrap)
|
||||
- AC-6 → `AuthContext.test.tsx` NFT-SEC-01 + FT-P-03 (401-retry path unchanged); plus existing
|
||||
`src/api/client.test.ts` retry tests
|
||||
|
||||
## Code Review Verdict: PASS
|
||||
|
||||
- Report: `_docs/03_implementation/reviews/batch_13_review.md`
|
||||
- 0 findings (Critical / High / Medium / Low)
|
||||
- Resolved baseline finding **B3** (Auth bootstrap missing `credentials:'include'` — Vision P3 violation)
|
||||
|
||||
## Auto-Fix Attempts: 0
|
||||
|
||||
No auto-fix loop needed.
|
||||
|
||||
## Stuck Agents: 0
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Changed Files
|
||||
|
||||
**Production code**:
|
||||
- `src/auth/AuthContext.tsx` — replaced GET-refresh `useEffect` with `runBootstrap()` POST +
|
||||
chained `/users/me`; added module-scoped `bootstrapInflight` for StrictMode safety; defensive
|
||||
`hasPermission` against legacy `/users/me` payloads missing `permissions`.
|
||||
- `src/auth/index.ts` — re-exports `__resetBootstrapInflightForTests` to keep tests off deep
|
||||
imports (STC-ARCH-01).
|
||||
- `src/api/endpoints.ts` — added `endpoints.admin.usersMe()` builder; STC-ARCH-02 forbids the
|
||||
literal `/api/admin/users/me` outside `endpoints.ts`.
|
||||
|
||||
**Tests** (handler swaps + new AC-4 + setup hook):
|
||||
- `src/auth/AuthContext.test.tsx` — un-quarantined FT-P-01 (now POST regression guard); updated
|
||||
FT-P-03 / NFT-SEC-01 / NFT-SEC-02 to POST refresh + chained `/users/me`; added AC-4 (AZ-510)
|
||||
test.
|
||||
- `src/auth/ProtectedRoute.test.tsx` — `withUser` helper now uses POST refresh + GET `/users/me`;
|
||||
all `http.get('/api/admin/auth/refresh', …)` mocks swapped to POST.
|
||||
- `src/components/Header.test.tsx` — `wireAuthAndFlights` updated to POST refresh + `/users/me`.
|
||||
- `src/api/endpoints.test.ts` — wire-contract assertion for `endpoints.admin.usersMe()`.
|
||||
- `tests/msw/handlers/admin.ts` — default `GET /users/me` handler returns user with explicit
|
||||
`permissions: seedPermissions[opAlice.id] ?? []` (was missing → caused
|
||||
`TypeError: Cannot read properties of undefined (reading 'includes')`).
|
||||
- `tests/setup.ts` — `afterEach` hook calls `__resetBootstrapInflightForTests` to prevent
|
||||
module-scoped inflight promise leakage between tests.
|
||||
- 15 broader test files (`tests/*.test.tsx`) — bulk swap of intentional-fail bootstrap
|
||||
handlers from `http.get` → `http.post` for `/api/admin/auth/refresh`. Without the swap the
|
||||
POST-based bootstrap would auto-authenticate from the default handler and break tests that
|
||||
expect `user: null`.
|
||||
|
||||
**Documentation**:
|
||||
- `_docs/02_document/components/02_auth/description.md` — bootstrap section rewritten to
|
||||
describe POST + chained `/users/me`; Finding B3 marked closed.
|
||||
|
||||
### Resolved Finding
|
||||
|
||||
- **B3** (`_docs/02_document/04_verification_log.md`): Auth bootstrap missing
|
||||
`credentials:'include'` — closed by AZ-510. Architecture Vision principle P3 ("bearer in
|
||||
memory, refresh in HttpOnly cookie") now satisfied on the bootstrap path.
|
||||
|
||||
### Test Run
|
||||
|
||||
- Static profile: PASS (all gates including STC-ARCH-01 / STC-ARCH-02 green)
|
||||
- Fast profile: 31 files, 231 passed / 13 skipped (quarantined). No new failures.
|
||||
- Suite duration: ~30s (fast) + ~55s (static).
|
||||
|
||||
### Notable Failure-Then-Fix Path During Implementation
|
||||
|
||||
1. **`ProtectedRoute.test.tsx` hangs (3 tests)** — module-scoped `bootstrapInflight` leaked
|
||||
the never-resolving promise from one test into subsequent renders. Fix: test-only export
|
||||
+ afterEach reset hook.
|
||||
2. **STC-ARCH-01 violation** — `tests/setup.ts` initially imported the test helper directly
|
||||
from `src/auth/AuthContext`. Fix: re-export through the `src/auth` barrel; switch import.
|
||||
3. **Widespread test failures** (`flight_selection_persistence.test.tsx`,
|
||||
`browser_support_responsive.test.tsx`, …) — default `/users/me` handler omitted
|
||||
`permissions`, so `hasPermission` crashed on `undefined.includes`. Fix: defensive
|
||||
`hasPermission` + handler now seeds `permissions` from `seedPermissions[opAlice.id]`.
|
||||
4. **Bulk handler swap** — 15 test files mocked `http.get('/api/admin/auth/refresh', …)` to
|
||||
force bootstrap fail. Production now uses POST so the GET override is ignored and bootstrap
|
||||
auto-authenticates from defaults. Fixed via per-file `sed` in a `for` loop (single `sed`
|
||||
with the full file list hit a shell command-line length issue and reported "No such file
|
||||
or directory").
|
||||
|
||||
## Next Batch
|
||||
|
||||
**Batch 14 (cycle 3 / batch 2 of 3)** — AZ-511 classColors carve-out to `src/class-colors/`
|
||||
(closes Finding F3 + 5-coupled-places exemption).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Code Review Report — Batch 13
|
||||
|
||||
**Batch**: AZ-510 (Auth bootstrap refresh consolidation)
|
||||
**Cycle**: 3
|
||||
**Date**: 2026-05-13
|
||||
**Verdict**: PASS
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Context Loading
|
||||
|
||||
- Task spec: `_docs/02_tasks/todo/AZ-510_auth_bootstrap_consolidation.md` — replace broken
|
||||
`GET /api/admin/auth/refresh` (no `credentials:'include'`) with `POST /api/admin/auth/refresh`
|
||||
(with credentials) chained to `GET /api/admin/users/me`. Closes Finding B3 / Vision P3.
|
||||
- Architecture vision principle P3 (`bearer in memory, refresh in HttpOnly cookie`) requires the
|
||||
bootstrap path to send the HttpOnly refresh cookie; the prior code violated this.
|
||||
- Architecture compliance baseline (`_docs/02_document/architecture_compliance_baseline.md`)
|
||||
carries B3 as the open downstream item AZ-510 was created to close.
|
||||
|
||||
## Phase 2: Spec Compliance
|
||||
|
||||
| AC | Mechanism | Test Evidence |
|
||||
|----|-----------|---------------|
|
||||
| AC-1 — POST refresh + `credentials:'include'`, no GET refresh | `runBootstrap()` direct `fetch(..., {method:'POST', credentials:'include'})` (`AuthContext.tsx:45-48`) | `AuthContext.test.tsx` FT-P-01 asserts method, credentials, chain |
|
||||
| AC-2 — Successful refresh chains to `/users/me` and resolves `loading:false` | `setToken(refreshData.token)` then `api.get<AuthUser>(endpoints.admin.usersMe())` (`AuthContext.tsx:51-53`); `setUser(result)` + `setLoading(false)` (`:78-79`) | FT-P-01 asserts `usersMeHits === 1`; `getToken()` becomes `BEARER` in NFT-SEC-01 |
|
||||
| AC-3 — Failed refresh → `/login` exactly once, no flash | `if (!refreshRes.ok) return null` (`:49`) → `setUser(null)` + `setLoading(false)` (`:78-79`) | `ProtectedRoute.test.tsx` covers spinner→`/login` paths under POST-refresh handlers |
|
||||
| AC-4 — `/users/me` failure clears bearer + logs | `try/catch` around `api.get` calls `setToken(null)` + `console.error` + returns `null` (`:54-61`); top-level `.then` then sets `user:null` + `loading:false` | New `AC-4 (AZ-510)` test in `AuthContext.test.tsx:108-138` asserts `getToken()` becomes `null`, `console.error` carries `"/users/me failed"` |
|
||||
| AC-5 — Returning user not bounced to `/login` | Successful bootstrap path sets `user` before `loading:false`; `ProtectedRoute` only redirects when `!loading && !user` | Implicit in `ProtectedRoute.test.tsx` admin-route success cases (no `/login` rendered) |
|
||||
| AC-6 — 401-retry path unchanged | `runBootstrap` uses direct `fetch`, not `api`; `api/client.ts:73-98` unchanged | `NFT-SEC-01` exercises bootstrap → 401 on `/users/me` → POST refresh rotation → replay; `FT-P-03` covers refresh transparency |
|
||||
|
||||
**Constraints**:
|
||||
- C1 `getApiBase()` is the only base-URL source — honored (`:45`).
|
||||
- C2 No `api.post()` for refresh — honored; uses direct `fetch` per the same comment in `api/client.ts:88`.
|
||||
- C3 MSW handlers exercise production paths — honored; no `vi.mock('api/client')`.
|
||||
- C4 `setToken(null)` precedes `setUser(null)` on every failure path — honored:
|
||||
- `/users/me` failure: `setToken(null)` (`:59`) → return `null` → top-level `setUser(null)` (`:78`).
|
||||
- Outer fetch reject: `setToken(null)` (`:87`) → `setUser(null)` (`:88`).
|
||||
|
||||
**Risk 4 (StrictMode double-mount)**: addressed via module-scoped `bootstrapInflight` promise
|
||||
(`AuthContext.tsx:25, 70-74`). Test-only escape hatch `__resetBootstrapInflightForTests`
|
||||
exported via the `src/auth` barrel and called in `tests/setup.ts` afterEach to prevent
|
||||
inter-test promise leakage (was the proximate cause of `ProtectedRoute.test.tsx` hangs during
|
||||
implementation).
|
||||
|
||||
No spec-gap findings.
|
||||
|
||||
## Phase 3: Code Quality
|
||||
|
||||
- **SOLID / SRP**: `runBootstrap` has one responsibility (refresh + chain + clear-on-failure);
|
||||
`AuthProvider`'s effect orchestrates the inflight guard and react state — clean separation.
|
||||
- **Error handling**: explicit `try/catch` around `/users/me`; outer `.catch` handles network
|
||||
errors on the POST refresh itself. Both log via `console.error` with diagnostic prefix.
|
||||
No bare catches introduced. (Pre-existing `try { await api.post(authLogout()) } catch {}` in
|
||||
`logout` is out of scope.)
|
||||
- **Naming**: `bootstrapInflight`, `runBootstrap`, `__resetBootstrapInflightForTests` are
|
||||
precise and self-documenting. Test export name carries the `__…ForTests` convention.
|
||||
- **Defensive `hasPermission`**: `user?.permissions?.includes(perm) ?? false` — correctly
|
||||
guards against legacy `/users/me` payloads that omit `permissions`. Required because
|
||||
several existing test fixtures returned the bare `User` shape without `permissions`.
|
||||
- **Comments**: comments explain *why* (StrictMode race, CORS posture for `api.post`,
|
||||
Constraint #4 ordering) — not *what*. Conforms to coderule.mdc.
|
||||
- **Test quality**: AC-4 test asserts `getToken() === null` AND that `console.error` was
|
||||
called with the diagnostic prefix — meaningful state + log assertion, not just "no throw".
|
||||
|
||||
No findings.
|
||||
|
||||
## Phase 4: Security Quick-Scan
|
||||
|
||||
- No hardcoded secrets, no SQL/string-interp queries, no `eval`/`exec`.
|
||||
- `console.error('[AuthContext] Refresh succeeded but /users/me failed:', err)` logs the error
|
||||
object. The error object originates from `api.get` which throws a structured error without
|
||||
bearer material; the bearer was set via `setToken` before the try block but is not in the
|
||||
thrown error. No bearer leak.
|
||||
- HttpOnly refresh cookie continues to flow via `credentials:'include'` — never touched in JS.
|
||||
NFT-SEC-02 explicitly verifies `document.cookie` carries no refresh-prefixed cookie.
|
||||
|
||||
No findings.
|
||||
|
||||
## Phase 5: Performance
|
||||
|
||||
- Two sequential network calls (POST refresh → GET `/users/me`) on every cold mount. Spec NFR
|
||||
budgets 200 ms p95 for the chain on dev compose; same nginx/auth/host. Within budget.
|
||||
- Module-scoped inflight promise prevents double-bootstrap under StrictMode dev double-mount,
|
||||
removing the wasted second round-trip.
|
||||
|
||||
No findings.
|
||||
|
||||
## Phase 6: Cross-Task Consistency
|
||||
|
||||
Single-task batch — N/A.
|
||||
|
||||
## Phase 7: Architecture Compliance
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Layer direction | `src/auth/AuthContext.tsx` imports from `../api` (barrel) and `../types` only — auth → api allowed per architecture |
|
||||
| Public API respect | All cross-component imports go through `src/api/index.ts` and `src/types/index.ts` barrels; no deep imports |
|
||||
| New cyclic deps | None introduced |
|
||||
| Duplicate symbols | None |
|
||||
| Cross-cutting in component dir | `bootstrapInflight` is auth-specific state; correctly lives in the auth component |
|
||||
|
||||
**STC-ARCH-01 (cross-component deep imports)** static gate: passed after fixing the
|
||||
`tests/setup.ts → src/auth/AuthContext` deep import by re-exporting
|
||||
`__resetBootstrapInflightForTests` from `src/auth/index.ts` (barrel) and switching the import
|
||||
to `../src/auth`.
|
||||
|
||||
**STC-ARCH-02 (no hardcoded API literals)** static gate: passed; new `endpoints.admin.usersMe`
|
||||
builder added (`src/api/endpoints.ts`) and used at the only callsite.
|
||||
|
||||
### Baseline Delta
|
||||
|
||||
| Status | Finding | Notes |
|
||||
|--------|---------|-------|
|
||||
| Resolved | B3 — Auth bootstrap missing `credentials:'include'` | Was open in `_docs/02_document/04_verification_log.md`; bootstrap now POST + `credentials:'include'` + chained `/users/me`. |
|
||||
| Carried over | (none in this file's scope) | — |
|
||||
| Newly introduced | (none) | — |
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** — no Critical / High / Medium / Low findings. All ACs covered with tests; constraints
|
||||
honored; static and fast profiles green (231 passed, 13 quarantined skips unchanged); Finding
|
||||
B3 resolved.
|
||||
@@ -6,9 +6,9 @@ step: 10
|
||||
name: Implement
|
||||
status: in_progress
|
||||
sub_step:
|
||||
phase: 0
|
||||
name: awaiting-invocation
|
||||
detail: ""
|
||||
phase: 14
|
||||
name: loop
|
||||
detail: "batch 2 of 3 (AZ-511 next)"
|
||||
retry_count: 0
|
||||
cycle: 3
|
||||
tracker: jira
|
||||
|
||||
Reference in New Issue
Block a user