Skip to content

Slice 3: Role-based access control + cross-role redirects #3

Description

@mGasiorek998

What to build

Server-side and client-side role enforcement on top of the authenticated baseline. A requireRole([...]) middleware composes after requireAuth() on the server and rejects with 403 unless the request's role is in the allow-list. On the client, TanStack Router beforeLoad hooks gate /patient and /doctor so a doctor visiting /patient is redirected to /doctor (and vice versa) — the boundary is enforced, but the UX is a gentle redirect rather than a 403 page. Hitting /login while authenticated also redirects to the user's correct dashboard so the back button never strands a logged-in user on the form.

End-to-end deliverables:

API

  • requireRole(allowedRoles: UserRole[]) middleware that runs after requireAuth(), reads role from the request context, returns 403 + { error: 'Forbidden' } if it is not in allowedRoles or is not a valid UserRole (defense-in-depth for EC-003 — junk role on a JWT must be denied even if the route allows it)
  • Two probe routes added for testability: GET /api/_probe/doctor-only (guarded by requireRole(['doctor'])) and GET /api/_probe/patient-only (guarded by requireRole(['patient'])). Each returns { ok: true } on success. These probes exist purely to prove the middleware works end-to-end and may be removed once Modules 01–05 introduce real role-guarded routes.
  • Structured log entry auth.rbac.denied written with user_id, role, route on every 403

Contracts

  • The probe routes get typed contract entries so the e2e can call them via the contract surface

Web

  • A reusable requireRole route-guard primitive — a TanStack Router beforeLoad helper that awaits the ['auth', 'me'] query and either lets the route render, redirects to /login (unauthenticated), or redirects to the user's correct dashboard (wrong role)
  • /patient route guarded by the patient role; doctor visiting /patient is redirected to /doctor
  • /doctor route guarded by the doctor role; patient visiting /doctor is redirected to /patient
  • /login route: if the ['auth', 'me'] query is fresh and successful, redirect to the user's role-appropriate dashboard before rendering the form
  • The unauthenticated-redirect-to-/login behavior from Slice 2 is preserved (and ideally consolidated into the same requireRole primitive without a role list = requireAuth)

Acceptance criteria

  • requireRole(['doctor']) allows a doctor's request and rejects a patient's request with 403 + { error: 'Forbidden' }
  • requireRole(['patient']) allows a patient's request and rejects a doctor's request with 403 + { error: 'Forbidden' }
  • A JWT carrying a role value other than 'doctor' or 'patient' is rejected with 403 even if the route's allowedRoles is permissive (proven by a unit test that signs a custom JWT with a fake role)
  • Every 403 from requireRole writes a structured log entry named auth.rbac.denied containing user_id, role, and the request path
  • On the web side, a logged-in patient who navigates to /doctor is redirected to /patient (Playwright e2e)
  • On the web side, a logged-in doctor who navigates to /patient is redirected to /doctor (Playwright e2e)
  • On the web side, a logged-in user who navigates to /login is redirected to their dashboard (Playwright e2e)
  • An unauthenticated request to either /patient or /doctor redirects to /login (regression check from Slice 2 still passes)
  • pnpm verify is green

User stories covered

  • US 10 from PRD: "As a patient who tries to open a doctor-only URL, I want to be denied and informed."
  • US 11 from PRD: "As a doctor who tries to open a patient-only URL, I want to be denied and informed."
  • US 12 from PRD: "As an unauthenticated visitor, I want to be redirected to the login page when I hit any protected URL."
  • US 21 from PRD: "As a developer writing a protected route in apps/api, I want a requireRole(['doctor']) (or ['patient']) helper."
  • US 22 from PRD: "As a developer writing a protected page in apps/web, I want a route-guard primitive that reads from the ['auth', 'me'] query."
  • US 28 from PRD: "As a security-aware developer, I want a JWT carrying a role outside 'doctor' | 'patient' to be rejected by the role guard regardless of route configuration."

Blocked by

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkEligible for the main agent looppriority:1Priority 1 — bugs default hereqa-readyPR opened, awaiting human QAsliceVertical tracer-bullet slice

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions