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

126 lines
9.4 KiB
Markdown

# 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`](LOOP-PROMPT.md). The full verified
finding ledger (with per-item checkboxes) is in [`issues.md`](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`](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](hardening-phase-0-auth-gate.md) | frontend | H-01 H-02 H-03 | — |
| **1** | [Admin RBAC & staff role scopes](hardening-phase-1-admin-rbac.md) | backend | H-04 H-05 (+delivers REQ-031) | — |
| **2** | [Money-path mock integrity](hardening-phase-2-mock-integrity.md) | frontend | H-06 H-07 H-08 H-09 H-10 | 0 |
| **3** | [Session & error-surface hardening](hardening-phase-3-session-error-ux.md) | frontend | H-11 H-12 H-13 H-14 | 0 |
| **4** | [Contract completion batch (Tier B/C)](hardening-phase-4-contract-completion.md) | backend | H-15 H-16(server half) | 1 |
| **5** | [Final de-mock & partner reachability](hardening-phase-5-final-demock.md) | 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](../../shared-working-context/README.md) 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](../../phases/_shared/phase-template.md)):
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](../../phases/_shared/agent-operating-rules.md).
## Related documents
- [issues.md](issues.md) — the verified finding ledger (evidence + checkboxes; the loop's progress state).
- [LOOP-PROMPT.md](LOOP-PROMPT.md) — the reusable prompt that drives this chain phase by phase.
- [../refinement/README.md](../refinement/README.md) — the prior chain this one follows.
- [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
— the REQ ledger Phase 4 closes out.
- [../../shared-working-context/reports/mocks-registry.md](../../shared-working-context/reports/mocks-registry.md)
— the mock registry Phases 2 & 5 update.