# 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.
