Hardening phases — auth gate, role enforcement & the last mocks
Created: 2026-07-16 · Scope: whole repo (client + server), after the 16+16 build phases and the 10 refinement phases all completed · Method: a 6-dimension multi-agent audit of the live code (anonymous access, role guarding, server authorization, remaining mocks, end-to-end journey walk, session/error UX), every finding adversarially re-verified against the code with file:line evidence, plus a live runtime probe of the dev server.
This directory is a runnable chain of 6 hardening phases. Run them in order, one at a time,
pointing a fresh agent at one phase file ("Execute dev/post-phase/hardening/hardening-phase-0-auth-gate.md
end to end") — or drive the whole chain with LOOP-PROMPT.md. The full verified
finding ledger (with per-item checkboxes) is in issues.md.
Why this exists after the refinement chain. The refinement phases made the two projects run as one app. But live probing shows the user-facing symptom "the app never asks me to log in, and roles feel unenforced" is real — caused by an auth gate that never executes at runtime, a token-liveness check that can't read the server's encrypted token, an admin RBAC policy that is structurally dead (every admin endpoint 403s), and two client money-path mocks left dangling by the phase-4 de-mock.
The headline: why the app never asks for login
Three defects mask each other, which is why this survived nine refinement phases:
client/middleware.tsnever executes in the running app (verified live: a cookie-lessGET /fa/adminreturns 200 with page HTML; a bareGET /returns 404 instead of next-intl's locale redirect — under both Turbopack and webpack). The gate code itself is correct. Root cause on this machine: a strayC:\Users\Lenovo\pnpm-lock.yaml(home directory, not in the repo) makes Next.js infer the workspace root as the home directory, so the middleware file is never bound. The repo must defend against this (pin the root innext.config.mjs) and prove the gate runs (runtime DoD in Phase 0). Next 16 has also deprecatedmiddleware.tsin favor ofproxy.ts.isTokenAlivecan never returntruefor a real token. The server's access token is an encrypted JWE (JwtService.cs:116-128,EncryptingCredentials, A128KW/A128CBC-HS256); the client helper (client/src/lib/auth/token.ts:24-38) base64-decodes segment 1 and JSON-parses it — impossible for a JWE. So had the middleware ever run, it would have redirect-looped logged-in users to /login. Same helper seeds server-side auth state (getServerAuthState), soisAuthenticatedseedsfalseon every hard reload. The wire already returnsaccessExpiresAt/refreshExpiresAt(client/src/services/auth/types.ts:65-66) — the fix is a readable companion expiry cookie.- The client fallback can't rescue an anonymous visitor.
useMe()is enabled only whenisAuthenticated; for a cookie-less request that'sfalseforever, souseRoleHydration()staysloadingand every private shell renders an infinite branded splash instead of redirecting to login. RoleGuard needs an explicit unauthenticated → redirect-to-login branch (defense in depth).
One manual step no phase can do for you: delete or move the stray C:\Users\Lenovo\pnpm-lock.yaml
from your home directory (it is unrelated to this repo). Phase 0 pins the workspace root so the app no
longer depends on that cleanup, but the stray file will keep confusing other tools too.
What's actually fine (don't re-fix)
- The middleware/auth-gate logic and
PUBLIC_PATHSare correct as written — the problem is execution- the JWE check, not the design.
- The 4 private shells genuinely all wrap
RoleGuard; admin mutations are consistently gated behinduseAdminCapabilities(). - Server tenancy is enforced correctly in every spot-checked handler (bookings, tickets, patients, care records, bank accounts, centers) — owner-or-staff checks with 404-not-403.
- Public endpoints (catalog/geo/search/nurses, webhooks, dev) are intentionally anonymous;
dev/last_otpcorrectly 404s outside Development. - The demo seed gives Journey A a real searchable Tehran nurse; base route names match 1:1 client↔server; the silent-refresh mechanism (single-flight, retry-once) is sound.
The verified problem inventory (17 findings + root cause)
Severity-ordered; the full ledger with evidence is issues.md.
| # | Severity | Problem | Phase |
|---|---|---|---|
| H-01 | blocker | Auth gate never executes at runtime (workspace-root misdetection; middleware deprecated) | 0 |
| H-02 | blocker | isTokenAlive can't read the JWE token → would redirect-loop; seeds isAuthenticated=false on reload |
0 |
| H-03 | high | Anonymous visitor to a private shell gets an infinite splash, never a login prompt | 0 |
| H-04 | blocker | DynamicPermission RBAC is dead: no RoleClaim ever seeded/grantable → every admin endpoint 403s for the seeded super_admin/finance personas |
1 |
| H-05 | high | BookingRoles.Admin bundles Support/Moderation into clinical-notes + nurse-balance + forced-transition access |
1 |
| H-06 | blocker | Refunds mock cross-imports the retired bookings-mock store → real cancellations 404 | 2 |
| H-07 | blocker | BNPL mock cross-imports retired mock stores → installment checkout 404s or fabricates a fake success while the real request expires unpaid | 2 |
| H-08 | blocker | Verification 100% mocked while catalog/search are real → a real nurse "publishes" services that can never appear in search, no feedback | 2 |
| H-09 | high | Nurse earnings screen fabricated although the REQ-025 endpoints are live (flag held hostage by the admin half of the seam) | 2 |
| H-10 | medium | Payment outcome hard-codes bookingId: null though REQ-017 is delivered → confirmation deep links lost |
2 |
| H-11 | high | Logout/login never clear the React Query cache → previous user's data leaks to the next login on the same device | 3 |
| H-12 | high | The only error boundary dumps a raw English stack trace, no retry; no error.tsx/global-error.tsx |
3 |
| H-13 | high | All 401/403/5xx/network toasts are hardcoded English on a Persian-default app | 3 |
| H-14 | medium | Admin read-only consoles (audit/verification/tickets/roles) render without a capability check — only the nav hides them | 3 |
| H-15 | medium | Tier B/C contract REQs still open (refunds 019-021, BNPL 022-024, admin 029-031, partner 032/033/038, verification admin 034, refund admin 035, payout admin 036) | 4 |
| H-16 | medium | Partner portal unreachable from login (no /me signal), fully mocked, no tenancy gate on its pages |
4+5 |
| H-17 | medium | patientRecords family record: client string ids vs wire long ids → PUT is write-unsafe; edits don't survive reload |
5 |
The 6 hardening phases
| # | Phase | Track | Fixes | Depends on |
|---|---|---|---|---|
| 0 | Auth gate & session liveness | frontend | H-01 H-02 H-03 | — |
| 1 | Admin RBAC & staff role scopes | backend | H-04 H-05 (+delivers REQ-031) | — |
| 2 | Money-path mock integrity | frontend | H-06 H-07 H-08 H-09 H-10 | 0 |
| 3 | Session & error-surface hardening | frontend | H-11 H-12 H-13 H-14 | 0 |
| 4 | Contract completion batch (Tier B/C) | backend | H-15 H-16(server half) | 1 |
| 5 | Final de-mock & partner reachability | frontend | H-16(client half) H-17 + flip the last flags | 2, 4 |
frontend: 0 auth gate ──► 2 mock integrity ──► 5 final de-mock
└───────► 3 session/error UX ▲
backend: 1 admin RBAC ──► 4 contract batch ──────┘
Phases 0 and 1 are independent — a frontend and a backend agent can run them in parallel (the shared-working-context protocol applies). Minimum path to "the app asks for login and roles hold": 0 → 1. Minimum path to "the money path works end-to-end on real data": 0 → 2. Everything real, no mocks: all six.
How the phase files are written
Same skeleton as the rest of the repo (the phase template): mission, context, required reading, enumerated scope with the audit's file:line evidence inlined (so the executing agent doesn't re-audit), invariants, Definition of Done, how to test, close-out. Before executing any phase, read _shared/agent-operating-rules.md.
Related documents
- issues.md — the verified finding ledger (evidence + checkboxes; the loop's progress state).
- LOOP-PROMPT.md — the reusable prompt that drives this chain phase by phase.
- ../refinement/README.md — the prior chain this one follows.
- ../../shared-working-context/frontend/requests/for-backend.md — the REQ ledger Phase 4 closes out.
- ../../shared-working-context/reports/mocks-registry.md — the mock registry Phases 2 & 5 update.