cleanup phases 6
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# Hardening Phase 0 — Auth gate & session liveness (make the app ask for login)
|
||||
|
||||
> Make the login gate actually execute at runtime, make token liveness readable despite the JWE access
|
||||
> token, and give anonymous visitors a login redirect instead of an infinite splash. This is the direct
|
||||
> fix for the reported symptom *"there is no ask for logging in."*
|
||||
> **Track:** frontend · **Depends on:** — · **Unlocks:** Phases 2, 3
|
||||
> **Before you start, read [_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).**
|
||||
|
||||
## 1. Context — where this sits
|
||||
|
||||
Fixes **H-01, H-02, H-03** from [issues.md](issues.md). Three defects mask each other: the middleware
|
||||
never runs (so nobody is asked to log in), its token check could never pass anyway (the token is an
|
||||
encrypted JWE), and the client fallback shows an infinite splash to anonymous visitors. The gate
|
||||
*design* (middleware + `PUBLIC_PATHS` + RoleGuard-as-chrome) is correct and stays.
|
||||
|
||||
**What already exists (do not rebuild):** `client/middleware.ts` (correct logic), `PUBLIC_PATHS`
|
||||
(`src/constants/routes.ts:169`), `RoleGuard`/`useRoleHydration`/`AuthAccountError`
|
||||
(refinement-phase-2), `persistAuthTokens`/`clearAuthTokens` (`src/lib/auth/session.ts`), the
|
||||
single-flight silent refresh (`src/lib/api/refresh.ts`), and the wire's
|
||||
`accessExpiresAt`/`refreshExpiresAt` (`src/services/auth/types.ts:62-66`).
|
||||
|
||||
## 2. Required reading (do this first)
|
||||
|
||||
- [issues.md](issues.md) H-01/H-02/H-03 — the verified evidence; don't re-audit.
|
||||
- `client/middleware.ts`, `client/src/lib/auth/token.ts`, `client/src/lib/auth/session.ts`,
|
||||
`client/src/lib/auth/server.ts`, `client/src/components/auth/RoleGuard.tsx`,
|
||||
`client/src/services/auth/hooks/useRoleHydration.ts`, `client/src/services/auth/routing.ts`.
|
||||
- `client/CLAUDE.md` → "Auth Cookies & session state" (the documented design you are repairing) and
|
||||
"Golden rules".
|
||||
- Next.js 16 `proxy.ts` file convention (middleware is deprecated/renamed; file must sit at the same
|
||||
level as `app` — for this repo that's `client/src/`).
|
||||
|
||||
## 3. Scope — build this
|
||||
|
||||
1. **Make the gate execute — and prove it.**
|
||||
- Pin the workspace root in `client/next.config.mjs`: `turbopack.root` (and
|
||||
`outputFileTracingRoot`) → the `client/` directory, so a stray lockfile in an ancestor directory
|
||||
(the live failure: `C:\Users\Lenovo\pnpm-lock.yaml`) can never re-point root detection.
|
||||
- Migrate `client/middleware.ts` → `client/src/proxy.ts` per the Next 16 convention (export
|
||||
`proxy`; keep the exact logic + matcher). Update the `client/CLAUDE.md` references.
|
||||
- **Runtime proof is part of the deliverable:** with the dev server running and no cookies,
|
||||
`GET /` must 307 to `/fa`, and `GET /fa`, `/fa/bookings`, `/fa/nurse`, `/fa/admin` must each
|
||||
redirect to `/fa/login`. `GET /fa/login` must 200. If the probe fails, keep working — do not
|
||||
declare done on a code-only fix.
|
||||
2. **JWE-compatible liveness.** The access token can't be decoded client-side, but verify_otp/refresh
|
||||
already return `accessExpiresAt`/`refreshExpiresAt`:
|
||||
- `persistAuthTokens` additionally writes a **non-sensitive companion cookie** (e.g.
|
||||
`access_expires_at`, name in `COOKIE_NAMES`) holding the ISO/epoch expiry; cookie `maxAge`s for
|
||||
both tokens derive from the served expiries (this also fixes the documented 7d-vs-server drift
|
||||
for the refresh cookie). `clearAuthTokens` deletes it.
|
||||
- Replace `isTokenAlive(token)` call sites (proxy + `getServerAuthState`) with a check that reads
|
||||
the expiry cookie when the token doesn't decode as a plain JWT (keep the JWT path as fallback so
|
||||
a future JWS still works). Token **presence** without a readable, live expiry = not alive.
|
||||
- Result: `getServerAuthState` seeds `isAuthenticated=true` after a hard reload of a logged-in
|
||||
session — verify `useMe`/`useSessionRoleSync` then hydrate roles normally.
|
||||
3. **Unauthenticated branch in the client fallback (defense in depth).**
|
||||
- `useRoleHydration` gains an explicit `unauthenticated` state (auth context says logged out);
|
||||
`RoleGuard` redirects it to `/${locale}/login?next=<current path>` instead of splashing forever.
|
||||
- The login flow honors `next`: after verify (+ role routing), `RoleRouter` prefers a safe,
|
||||
same-origin relative `next` path over `resolveRoleDestination` when present. Never redirect to
|
||||
an absolute/external URL.
|
||||
- `clientFetch`'s unrecoverable-401 redirect should also carry `next`.
|
||||
|
||||
## 4. Mocks & seams in this phase
|
||||
|
||||
None introduced. Do not touch `USE_*_MOCK` flags here (Phase 2 owns them).
|
||||
|
||||
## 5. Critical rules you must not get wrong
|
||||
|
||||
- **The middleware/proxy stays UX-only** — no signature verification client-side; the API remains the
|
||||
authority. Don't try to decrypt the JWE in the client.
|
||||
- The expiry cookie is a **liveness hint, not a credential** — never gate real authorization on it.
|
||||
- `PUBLIC_PATHS` uses `startsWith` — never add `'/'` or another prefix-of-everything entry.
|
||||
- Don't break the locale flow: the i18n redirect handling and locale header in the current middleware
|
||||
must survive the migration exactly.
|
||||
- Keep RoleGuard's existing loading/error/mismatch semantics (refinement-phase-2) — you're adding a
|
||||
fourth branch, not rewriting the guard.
|
||||
- Golden rules: no hardcoded strings (new copy → both `messages/*.json`), cookie access only via the
|
||||
cookie manager, constants for names/params.
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
- The runtime probe in §3.1 passes (paste the curl/status output into your report).
|
||||
- Logged-in user: hard reload on `/fa/bookings` stays there (no splash-hang, no bounce to login).
|
||||
- Logged-out user: any private deep link → login → completes OTP → lands back on the deep link.
|
||||
- Logout → immediately bounced to login on the next private navigation.
|
||||
- `npm run check` green; `npm run test:ci` green; RoleGuard + routing tests extended for the
|
||||
`unauthenticated` branch and the `next` param.
|
||||
|
||||
## 7. How to test
|
||||
|
||||
1. `cd client && npm run dev` (server up per `dev/post-phase/refinement/RUNBOOK.md`).
|
||||
2. No cookies: `curl -I http://localhost:3000/fa/bookings` → 307/308 `Location: /fa/login?next=…`.
|
||||
3. Log in (dev OTP via `GET /api/v1/dev/last_otp/{phone}`), F5 on the home shell → stays, roles load.
|
||||
4. Delete only the `access_token` cookie, navigate → silent refresh recovers; delete both → login ask.
|
||||
5. Visit `/fa/nurse` as a customer-only session → RoleGuard redirect toast (unchanged behavior).
|
||||
|
||||
## 8. Hand off & document
|
||||
|
||||
- Update `client/CLAUDE.md` (middleware→proxy path, expiry-cookie lifecycle, the JWE note) — its
|
||||
"middleware.ts" references and cookie table must match reality.
|
||||
- Tick H-01/H-02/H-03 in [issues.md](issues.md) with the commit hash.
|
||||
- Write `dev/shared-working-context/reports/hardening-phase-0-report.md` (what was verified at
|
||||
runtime, any surprises).
|
||||
- **Note to the human:** the stray `C:\Users\Lenovo\pnpm-lock.yaml` should still be deleted manually;
|
||||
the root pin makes the app immune, but other tools may not be.
|
||||
Reference in New Issue
Block a user