Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03 — User Journeys (RBAC Module)

Five end-to-end journeys with Mermaid diagrams. Every step maps to a real endpoint (rbac.controller.ts), a service rule (rbac.service.ts), or a guard behaviour (rbac.guard.ts). Personas per 02.


J1 — Create a custom role (Priya)

Entry: Roles list → FAB "Create role" → Role editor → Save.

sequenceDiagram
  autonumber
  actor A as Priya (org_admin)
  participant C as Client
  participant API as /api/v1/rbac
  A->>C: Opens Roles list
  C->>API: GET /rbac/permissions (catalog, 95 perms)
  API-->>C: data:[ "organization.read", ... ]  (permissions.constants.ts:1-97)
  C->>API: GET /rbac/roles (existing roles, priority desc)
  API-->>C: data:[{name,slug,isSystem,priority,permissions[]}]
  A->>C: FAB → editor: name, slug, description, priority, matrix check-off
  C-->>A: validations: slug pattern, ≥1 perm warning (not blocking), isSystem:false forced
  A->>C: Save
  C->>API: POST /rbac/roles {name,slug,description,isSystem:false,priority,permissions}
  alt 409 duplicate slug (role.schema.ts:89)
    API-->>C: {error.code:"DUPLICATE_RESOURCE"}
    C-->>A: inline "Slug already exists" + focus slug
  else 201/200
    API-->>C: data: role doc
    C-->>A: Snackbar "Role created" → navigate Role detail
  end

Rules exercised: createRole conflict check (rbac.service.ts:85-87); @Roles('org_admin') at class level (rbac.controller.ts:21); create is tenant-scoped via createWithTenant (role.repository.ts:17-22).

J2 — Assign permissions via the matrix (Priya)

Entry: Role detail → Edit permissions → matrix → Save. Matrix = hero component (06 §3).

flowchart TD
  S[Role detail: 'Fee Counter Clerk'] --> E[Edit permissions]
  E --> G[Matrix renders: 23 groups × chips, server catalog + current role perms]
  G --> F{User actions}
  F -->|Search 'pay'| SR[Filtered rows: payments.read, payments.process, payments.refund, payments.reconcile, receipts.read]
  F -->|Tap group header 'Finance'| GA[Group select-all toggle]
  F -->|Tap chip| TC[Toggle perm on/off, live count badge '6 of 95']
  SR --> TC
  GA --> TC
  TC --> D{Save}
  D -->|Cancel| E
  D -->|Save| P[PATCH /rbac/roles/:id {permissions:[...]}]
  P -->|200| R[Detail refreshes; perm chips updated; route tree of holders NOT rebuilt here]
  P -->|403 PERMISSION_DENIED| E3[403 inline: "You can't edit roles"]

Rules exercised: update refused for isSystem (rbac.service.ts:94-95); permission list sent wholesale (no per-cell endpoint — updateRole is full-doc $set, rbac.service.ts:96); effective permissions change for holders ≤ 300 s after cache TTL (rbac.service.ts:68) but JWT roles unchanged until re-login (auth.service.ts:145-154).

J3 — Add a member (Priya)

Entry: Members list → FAB "Add member" → pick user + roles → Add.

sequenceDiagram
  autonumber
  actor A as Priya
  participant C as Client
  participant API as /api/v1/rbac
  participant U as /api/v1/users (read)
  A->>C: Members list (GET /rbac/members, joinedAt desc)
  C->>API: GET /rbac/members
  API-->>C: data:[{userId,roles[],status,joinedAt,...}] (no profile join, rbac.service.ts:109-111)
  C->>U: GET /api/v1/users (profile merge for display, OQ-R7)
  A->>C: FAB → Add member sheet: search users, pick one, pick role chips
  C->>API: POST /rbac/members {userId, roles:[...]} (add-member.dto.ts:4-12)
  alt user already member (unique index org-member.schema.ts:48)
    API-->>C: 500 (Mongo E11000, unmapped — OQ-R5)
    C-->>A: conflict-styled banner "Already a member" (client-side pre-check)
  else 201/200
    API-->>C: data: member doc (status "active", joinedAt now — rbac.service.ts:122-123)
    C-->>A: row appears at top; Snackbar "Member added"
  end

J4 — Custom role for Accountant/HR, then assign (Priya, J1+J3 combined)

Entry: Roles → create "HR Officer" → Members → assign.

flowchart LR
  A[Create role 'HR Officer'] -->|POST /rbac/roles| B[isSystem:false, priority 45]
  B --> C[Select perms: user.read, user.create, user.update, staff.read, staff.create, staff.update, notification.read]
  C --> D[Roles list shows HR Officer above Teacher? No - priority 45 < teacher 50]
  D --> E[Members → Add member → pick Kamal → roles: hr_officer]
  E --> F[POST /rbac/members {userId, roles:['hr_officer']}]
  F --> G[Kamal re-login → JWT roles:['hr_officer'] auth.service.ts:145-154]
  G --> H[Kamal opens Staff list → allowed via @Permissions('staff.read') when Phase-5 lands (planned)]

Note (derived): priority is display-ordering only (rbac.service.ts:76); it does not influence authorization — authorization comes from JWT roles + permission resolution.

J5 — Guard-denied flow (David, teacher; also PLAN.md:17 1.5)

Entry: Teacher taps an admin-only action (or a stale route after his role was edited mid-session).

sequenceDiagram
  autonumber
  actor D as David (teacher)
  participant C as Client
  participant G as Guard chain
  actor A as Priya (admin)
  A->>G: PATCH /rbac/roles/:id removes 'teacher' from org (role edited at lunch)
  D->>C: Opens Fees screen (stale route from login-time JWT)
  C->>G: GET /api/v1/fees (JWT still claims roles:['teacher'] — auth.service.ts:145-154)
  G-->>G: JwtAuthGuard OK → RbacGuard: no @Permissions on fees controller today (OQ-R1/12§4) → ALLOWED (rbac.guard.ts:29)
  D->>C: Opens Webhooks screen
  C->>G: POST /webhooks requires @Permissions('webhook.create') (webhooks.controller.ts:22)
  G-->>G: getPermissionsForUser → Redis miss → DB: teacher role lacks webhook.create
  G-->>C: 403 ForbiddenException 'Insufficient permission.' (rbac.guard.ts:48-49)
  C-->>D: 403 screen: cause copy + "Ask your Organization Admin" + requestId; no stack

Covered by QA plan: PLAN.md:17 (1.5 RBAC admin hits teacher-only endpoint → 403), PLAN.md:107-110 (10.2 cross-tenant JWT → 403), PLAN.md:146-148 (14 soft-delete).