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>
7.3 KiB
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 brokenGET /api/admin/auth/refresh(nocredentials:'include') withPOST /api/admin/auth/refresh(with credentials) chained toGET /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 directfetchper the same comment inapi/client.ts:88. - C3 MSW handlers exercise production paths — honored; no
vi.mock('api/client'). - C4
setToken(null)precedessetUser(null)on every failure path — honored:/users/mefailure:setToken(null)(:59) → returnnull→ top-levelsetUser(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:
runBootstraphas one responsibility (refresh + chain + clear-on-failure);AuthProvider's effect orchestrates the inflight guard and react state — clean separation. - Error handling: explicit
try/catcharound/users/me; outer.catchhandles network errors on the POST refresh itself. Both log viaconsole.errorwith diagnostic prefix. No bare catches introduced. (Pre-existingtry { await api.post(authLogout()) } catch {}inlogoutis out of scope.) - Naming:
bootstrapInflight,runBootstrap,__resetBootstrapInflightForTestsare precise and self-documenting. Test export name carries the__…ForTestsconvention. - Defensive
hasPermission:user?.permissions?.includes(perm) ?? false— correctly guards against legacy/users/mepayloads that omitpermissions. Required because several existing test fixtures returned the bareUsershape withoutpermissions. - 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() === nullAND thatconsole.errorwas 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 fromapi.getwhich throws a structured error without bearer material; the bearer was set viasetTokenbefore 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 verifiesdocument.cookiecarries 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.