6.2 KiB
Hardening Phase 3 — Session & error-surface hardening
Close the shared-device data leak (logout leaves every domain's cache warm for the next login), turn the raw stack-trace error boundary into a branded recovery surface with route-level error pages, and localize the four hardcoded-English failure toasts. Plus: client-side capability gating for the four admin consoles that render without a check. Track: frontend · Depends on: Phase 0 · Unlocks: — (independent polish, safe anytime after 0) Before you start, read _shared/agent-operating-rules.md.
1. Context — where this sits
Fixes H-11 … H-14 from issues.md. All four are verified, none is touched by any other phase, and none needs backend work.
What already exists (do not rebuild): useLogout as the single logout path, the
AuthAccountError recovery-card pattern (mirror it for the boundary), dispatchToast/ToastBridge
(the non-React toast bridge — keep it, localize what flows through it), useAdminCapabilities()
(src/hooks/capabilities.ts) and the non-leaking access-denied patterns used elsewhere in the admin
screens.
2. Required reading (do this first)
- issues.md H-11/H-12/H-13/H-14 — evidence; don't re-audit.
client/src/services/auth/hooks/{useLogout,useVerifyOtp,useSelectRole}.ts,client/src/lib/query/{queryClient.ts,QueryProvider.tsx},client/src/components/common/ErrorBoundary.tsx+ its two consumers,client/src/lib/api/client.ts+client/src/lib/toast/*, the four admin pages (admin/{audit,verification,tickets,roles}/page.tsx) +AdminLayout.tsx.client/CLAUDE.md→ error contract forclientFetch, i18n rules, unit-testing rule.- Next.js
error.tsx/global-error.tsxfile conventions.
3. Scope — build this
- H-11 — cache isolation across sessions.
useLogout.onSettled:queryClient.clear()(replace the auth-onlyremoveQueries). Symmetrically clear/reset before seeding the new session inuseVerifyOtp.onSuccessanduseSelectRole.onSuccess, so User B never sees User A's cached patients/bookings/addresses/tickets/notifications on a shared device. Check nothing depends on surviving cache entries across logout (geo/catalog reference data may simply refetch — correct). - H-12 — real error surfaces.
- Rebuild
ErrorBoundary's fallback as a branded, i18n'd recovery card (mirrorAuthAccountError): translated generic message + retry (re-render children / reload), rawerror.toString()+ component stack rendered only in development. Keep the class component; add the error-reporting hook point (a single function you can later wire to a service — no TODO comment left behind). - Add
src/app/[locale]/error.tsxandglobal-error.tsxso errors thrown above the shells (RoleGuard, providers, layouts) get the same branded treatment instead of Next's default. Respect the root-layout constraint (global-errormust render its own<html>; keep locale/dir sane).
- Rebuild
- H-13 — localize the failure toasts.
lib/api/client.ts:57,80,86,91: replace the four English literals with a small locale-keyed dictionary module (the fetch layer already knows the locale; it can't calluseTranslations). Add keys (suggest anerrorsnamespace) to bothmessages/en.jsonandmessages/fa.json. Keep the error contract exactly (which statuses toast, which throw) — only the copy source changes. - H-14 — capability-gate the four admin consoles. Add a shared
CapabilityGuard(need: keyof AdminCapabilities) that early-returns the existing non-leaking access-denied state, and wrap the page bodies ofadmin/audit(canViewAudit),admin/verification(canVerify),admin/tickets(canManageTickets),admin/roles(canManageRoles) — before their data hooks fire. Display convenience, not security (the server enforces after Phase 1) — but a scoped admin deep-linking must see the denied card, not the data.
4. Mocks & seams in this phase
None.
5. Critical rules you must not get wrong
queryClient.clear()on logout must run after the server revoke call is issued (keep the existing revoke → clear-tokens → dispatch → redirect ordering; the cache clear joins it, doesn't reorder it).- Don't toast inside hooks for 401/403/5xx (already toasted by
clientFetch) — unchanged rule. global-error.tsxreplaces the root layout when it triggers — it must be self-contained (no providers assumed), bilingual-safe, and never crash itself.- Every new user-visible string in both message files; shared components (
CapabilityGuard, the new boundary fallback if extracted) get co-located tests per the client testing rule. - Do not turn the boundary into a swallow-everything — rethrow/log in dev so DX doesn't regress.
6. Definition of Done
- Shared-device test: login A (customer with patients) → logout → login B → B never sees A's data (verify React Query devtools cache is empty right after login).
- Throw a test error inside a page in dev → branded Persian card with retry; stack visible in dev build only. Route-level error files exist and render.
- On
fa, kill the API and trigger a fetch → Persian network-error toast; expire the session → Persian session-expired toast + login redirect (Phase 0 behavior). - Finance-scoped admin deep-links
/admin/audit→ access-denied card, no data fetch fired. npm run check+npm run test:cigreen (new tests for CapabilityGuard + boundary fallback).
7. How to test (human)
- Two seeded accounts on one browser: full logout/login swap watching the patients screen.
- Temporarily
throw new Error('boom')in a page render; check both dev andnpm run build && start. - Stop the API mid-session on
/fa; watch toast language; restart, expire tokens, watch the redirect. - Login as 09120000021 (finance), deep-link the four consoles.
8. Hand off & document
- Tick H-11…H-14 in issues.md with commit hashes.
- Update
client/CLAUDE.md(error contract copy source, error.tsx files in the structure tree, logout lifecycle). - Write
dev/shared-working-context/reports/hardening-phase-3-report.md.