# Refinement Phase 2 — Auth & role-aware navigation (the "only customer side" fix) > **Mission:** fix the most visible symptom — "there are nurse and admin pages, but running the frontend only > ever shows the customer side." Auth is already real; the reason you only see the customer app is that a > fresh session has only the `customer` role, role hydration silently falls back to `customer`, and nothing > routes a nurse/admin to their shell. This phase makes login, role hydration, and role-aware navigation work > end-to-end for all three actors. > > **Track:** integration (mostly frontend + a little backend) · **Depends on:** > [Phase 0](refinement-phase-0-bring-up.md), [Phase 1](refinement-phase-1-database-and-seed.md) · > **Unlocks:** reaching the nurse & admin experiences at all > **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** ## 1. Context — where this sits Auth is the **one domain already wired to the real backend** (`USE_AUTH_MOCK = false`). The phone-OTP flow, session cookies, silent refresh, and `/me` hydration all work. So why is only the customer app visible? Diagnosed root causes (all verified in code): 1. **A fresh phone login has only the `customer` role.** `resolveRoleDestination` (`client/src/services/auth/routing.ts`) sends a customer to `/` (the customer app lives at the app root — the `(customer)` route group has no URL segment) and only sends to `/nurse` / `/admin` if `/me.roles` actually contains `nurse`/`admin`. A brand-new user holds neither until they **self-select the nurse role** (`POST /api/v1/me/select_role`) or an admin grants a staff role. Nothing in the current run does that, so everyone lands on the customer app. 2. **A hard-coded `customer` fallback masks un-hydrated / failed role state.** `client/src/constants/roles.ts` → `DEFAULT_ROLE = customer`; `useActorRole()` (`client/src/hooks/auth.ts:29`) returns it whenever roles are empty. So if `useSessionRoleSync()` hasn't hydrated yet, or `/me` errored (backend down), **every private screen resolves to the customer shell** — a nurse can be silently shown the customer app. 3. **No client-side role guard on the shells.** `(customer)`, `/nurse`, `/admin`, `/partner` layouts each just wrap their shell; anyone can navigate to any of them (the server still enforces per-endpoint auth, but the *navigation/chrome* isn't role-aware, so the experience is confusing). 4. **Roles depend entirely on a successful client `/me`.** The server seed (`client/src/lib/auth/server.ts`) only sets `isAuthenticated` (the JWE is opaque server-side); roles come only from the client `/me` call in `useSessionRoleSync`. If that call fails, roles never populate. **What already exists (do not rebuild):** `resolveRoleDestination`, `RoleRouter`, `SelectRole`, `useSessionRoleSync`, `useActorRole`, the three shells, and `POST me/select_role`. This phase makes them *robust and reachable* — it does not rebuild the auth flow. ## 2. Required reading (do this first) - `client/src/services/auth/routing.ts` (`resolveRoleDestination`), `client/src/components/auth/RoleRouter.tsx`, `client/src/components/auth/SelectRole.tsx`. - `client/src/services/auth/hooks/useSessionRoleSync.ts`, `client/src/services/auth/hooks/useMe.ts`. - `client/src/hooks/auth.ts` (`useActorRole`, and the `useAdminCapabilities` roleCodes gating), `client/src/constants/roles.ts` (`DEFAULT_ROLE`). - `client/src/app/[locale]/(private-routes)/layout.tsx` (mounts `useSessionRoleSync`) and the four shell layouts under it (`(customer)`, `nurse`, `admin`, `partner`). - Server side: `Controllers/V1/MeController` (`/me`, `select_role`) and how `MeResult` exposes `roles` + `roleCodes`; the role vocabulary in `Domain/Entities/User/RoleNames`. - REQ-004 in `dev/shared-working-context/frontend/requests/for-backend.md` (the "client owns the active-role choice" decision to confirm) and REQ-031 (RBAC grant endpoints — needed if you want to grant staff roles from the UI rather than by seed). ## 3. Scope — build this ### 3.1 Make all three actor sessions *reachable* in development - **Nurse:** verify the real `POST /api/v1/me/select_role { role: "nurse" }` path — a customer taps "become a nurse" (the B1 switch / SelectRole) → `/me` now returns `["customer","nurse"]` → `resolveRoleDestination` routes to `/nurse`. Prove this end-to-end against the real backend. (The [Phase 1](refinement-phase-1-database-and-seed.md) demo seed can also pre-make nurse-roled phone accounts so you can log straight into `/nurse`.) - **Admin:** admin sub-roles are **not** self-selectable (`select_role` 403s for them, by design). Provide a path to an admin session: either the Phase 1 seed grants a demo phone user the `admin` role, or (better, and it's on the roadmap anyway) deliver REQ-031's `admin_roles/grant_role` so an existing admin can grant staff roles from `/admin/roles`. Pick one and document it. Confirm `/me` returns the fine-grained `roleCodes` the admin console's `useAdminCapabilities()` gates on (e.g. `moderation`, `finance`). - **Partner:** a partner-center admin is a separate authz scope (`useMyPartnerCenter`). Ensure a demo user is associated with a seeded partner center (Phase 1) so `/partner` resolves instead of access-denied. ### 3.2 Harden role hydration so it never silently mis-shells - Distinguish **"roles not yet loaded"** from **"user has no nurse/admin role."** While `useMe` is in-flight on a private route, render a neutral loading state (not the customer shell) so a nurse is never flashed the wrong app. Only fall back to `DEFAULT_ROLE` once `/me` has actually resolved with an empty/customer role set. - If `/me` **fails** (backend down), surface an explicit "couldn't load your account" state rather than silently defaulting to customer — otherwise a transient error downgrades a nurse/admin to the customer app. - Keep `resolveRoleDestination` the single source of truth for "which app" — don't scatter role checks. ### 3.3 Role-aware navigation guards (chrome, not security) - Add a lightweight guard so navigating to a shell the user lacks the role for **redirects** (nurse → `/nurse` when they hold `nurse`; a pure customer hitting `/nurse` is redirected home with a toast). The server remains the security boundary; this is UX so the actor experience matches the session. - For a dual-role user, honor the client-owned intended role (the A1 vs B1 login switch) per REQ-004's "client owns it" decision, and let them switch actor from the shell. ### 3.4 Backend confirmations / small adds (coordinate with Phase 3) - **REQ-004** (zero code): write the "client owns the active-role choice" decision into the tracker so the router behavior is contract-blessed. - If you chose the RBAC route in 3.1: **REQ-031** `admin_roles/list|grant|revoke` (this is the one net-new backend surface this phase may need; otherwise it's a Phase 1 seed). ## 4. Mocks & seams in this phase None. Auth is already real. Do **not** reach for `USE_AUTH_MOCK = true` to demo roles — that mock's `MOCK_SCENARIO` toggle is an offline convenience, and using it here would hide the real hydration bug this phase exists to fix. The whole point is that real `/me` roles drive navigation. ## 5. Critical rules you must not get wrong - **Never treat "roles still loading" as "customer."** That conflation is the core bug — a nurse gets the customer app for a beat (or forever, if `/me` fails). Gate on a resolved-vs-pending distinction. - **Admin roles are server-granted, never self-selected.** `select_role` must keep 403-ing for staff roles; reach admin via seed or RBAC grant, not by loosening `select_role`. - **Client guards are UX, not security.** Keep every server endpoint's authorization intact; the guard only redirects the browser to the right shell. - **i18n both locales** for any new copy (loading/mis-role/redirect states). ## 6. Definition of Done On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): - [ ] `npm run check` green; new shared components have `*.test.tsx`; `en.json`/`fa.json` in sync. - [ ] Logging in as a **nurse-roled** phone user lands on `/nurse`; an **admin** on `/admin`; a **partner** admin on `/partner`; a plain **customer** on `/`. - [ ] A customer who selects the nurse role (`select_role`) is routed to `/nurse` after the next `/me`. - [ ] Navigating to a shell you lack redirects (with a toast), not a broken page. - [ ] With the backend momentarily stopped, a logged-in nurse sees a loading/error state — **not** the customer app — and recovers to `/nurse` when `/me` succeeds. ## 7. How to test (what a human can verify after this phase) 1. Log in as the seeded **verified nurse** phone account → land on `/nurse`, see the nurse shell + dashboard. 2. Log in as a fresh customer → land on `/` (customer app). Tap "become a nurse" (SelectRole) → confirm `POST /me/select_role` in the Network tab → after `/me` refetch you're routed to `/nurse`. 3. Log in as the seeded **admin** → land on `/admin`; confirm the sidebar shows only the consoles your `roleCodes` allow (`useAdminCapabilities`). 4. As a pure customer, manually visit `/nurse` → redirected home with a toast. 5. Stop the API, reload a nurse session → loading/error state (not the customer app); restart → recovers. ## 8. Hand off & document (close the phase) - Update `client/CLAUDE.md` (the auth/role section) to describe the resolved-vs-pending hydration rule and the role-aware redirects; update the "Project Structure" note if you add a guard component. - Answer REQ-004 in the tracker; if you built RBAC grant, mark REQ-031 delivered. - Report the exact demo credentials for each actor and the role-reachability path. Save a memory note that the "only customer side" symptom was role-reachability + silent `customer` fallback, now fixed.