03 — User Journeys (RBAC Module)
- J1 — Create a custom role (Priya)
- J2 — Assign permissions via the matrix (Priya)
- J3 — Add a member (Priya)
- J4 — Custom role for Accountant/HR, then assign (Priya, J1+J3 combined)
- J5 — Guard-denied flow (David, teacher; also PLAN.md:17 1.5)
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).