some ui phase improvement planning
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user