Files
baya-monorepo/archive/post-phase/refinement/refinement-phase-2-auth-and-role-nav.md
T
2026-08-02 18:48:32 +03:30

143 lines
9.8 KiB
Markdown

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