Files
baya-monorepo/dev/post-phase/hardening

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:

  1. client/middleware.ts never executes in the running app (verified live: a cookie-less GET /fa/admin returns 200 with page HTML; a bare GET / 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 stray C:\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 in next.config.mjs) and prove the gate runs (runtime DoD in Phase 0). Next 16 has also deprecated middleware.ts in favor of proxy.ts.
  2. isTokenAlive can never return true for 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), so isAuthenticated seeds false on every hard reload. The wire already returns accessExpiresAt/refreshExpiresAt (client/src/services/auth/types.ts:65-66) — the fix is a readable companion expiry cookie.
  3. The client fallback can't rescue an anonymous visitor. useMe() is enabled only when isAuthenticated; for a cookie-less request that's false forever, so useRoleHydration() stays loading and 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_PATHS are 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 behind useAdminCapabilities().
  • 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_otp correctly 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.