Files
baya-monorepo/dev/post-phase/hardening/hardening-phase-3-session-error-ux.md
T
2026-07-17 13:22:04 +03:30

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 for clientFetch, i18n rules, unit-testing rule.
  • Next.js error.tsx / global-error.tsx file conventions.

3. Scope — build this

  1. H-11 — cache isolation across sessions. useLogout.onSettled: queryClient.clear() (replace the auth-only removeQueries). Symmetrically clear/reset before seeding the new session in useVerifyOtp.onSuccess and useSelectRole.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).
  2. H-12 — real error surfaces.
    • Rebuild ErrorBoundary's fallback as a branded, i18n'd recovery card (mirror AuthAccountError): translated generic message + retry (re-render children / reload), raw error.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.tsx and global-error.tsx so errors thrown above the shells (RoleGuard, providers, layouts) get the same branded treatment instead of Next's default. Respect the root-layout constraint (global-error must render its own <html>; keep locale/dir sane).
  3. 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 call useTranslations). Add keys (suggest an errors namespace) to both messages/en.json and messages/fa.json. Keep the error contract exactly (which statuses toast, which throw) — only the copy source changes.
  4. 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 of admin/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.tsx replaces 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:ci green (new tests for CapabilityGuard + boundary fallback).

7. How to test (human)

  1. Two seeded accounts on one browser: full logout/login swap watching the patients screen.
  2. Temporarily throw new Error('boom') in a page render; check both dev and npm run build && start.
  3. Stop the API mid-session on /fa; watch toast language; restart, expire tokens, watch the redirect.
  4. 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.