Files
baya-monorepo/dev/post-phase/refinement/refinement-phase-2-auth-and-role-nav.md
T
2026-07-10 20:59:47 +03:30

9.8 KiB

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, Phase 1 · Unlocks: reaching the nurse & admin experiences at all Before you start, read ../../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.tsDEFAULT_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 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:

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