06 — Screen Specifications (RBAC Module)
- S1 — Roles List
/settings/roles - S2 — Role Detail
/settings/roles/:id - S3 — Role Editor (permission matrix)
/settings/roles/new·/settings/roles/:id/edit - S4 — Members List
/settings/members - S5 — Add Member
/settings/members/add(bottom sheet) - S6 — Permission Audit View
/settings/access-audit
Production-grade specs for all six screens (S1–S6, 05_Screen_Inventory.md). Tokens:
Sp= 00-shared/02 §3,Ty= §2,El= §5,R= §4,Mo= §6. Components reference 00-shared/03 and 07_Component_Library.md (this module). The permission matrix (§3, S3) is the module's hero component and gets the deepest spec.
S1 — Roles List /settings/roles
Chrome: AppBar (title "Roles & Permissions", back); headlineMedium; list area.
Layout (phone):
AppBar "Roles & Permissions"
SectionHeader "System roles" (titleSmall, onSurfaceVariant)
RoleRow × 7 (locked) [AppAvatar shield] Name · slug (mono) [System][N perms] ⋮
SectionHeader "Custom roles"
RoleRow × N name · description (1 line) [N perms] ⋮
FAB "Create role" (extended)
Elements & behaviour:
RoleRow: leadingAppAvatar(initials of role name,primaryContainer); title = rolename(role.schema.ts:69-70); subtitle = slug inmono+ description (role.schema.ts:75-76) ellipsized; trailing =AppBadge"System" (locked,outline) or perm countAppBadge "{n} perms";AppMenu: custom → Edit / Delete; system → disabled (tooltip "System role").- Sections split by
isSystem(role.schema.ts:78-79); ordering priority desc (rbac.service.ts:76) within each section — Platform Admin (1000) then Org Admin (100) then Teacher (50)… in system; custom roles sorted by their priority. - Search:
AppSearchBarclient-filters name/slug (list is small; server has noq). - Delete:
AppDialogconfirm ("Delete role '{name}'? N members hold this role — they will lose its permissions within 5 minutes.") →DELETE /rbac/roles/:id(soft delete,rbac.service.ts:106) → optimistic remove + snackbar. - FAB: "Create role" →
/settings/roles/new.
States: loading AppSkeleton(list); error AppErrorState (UNAUTHENTICATED →
refresh flow; PERMISSION_DENIED → 403 screen); empty → only system section renders,
empty-state card "No custom roles yet".
A11y: system badge read as "System role — locked"; row semantics button; menu
actions labelled ("Edit role", "Delete role").
Tablet/desktop (≥ 840 dp): master-detail — list left (width 360), detail pane right
(go_router split). Hover states; keyboard ↑↓ moves selection, Enter opens detail.
S2 — Role Detail /settings/roles/:id
Chrome: AppBar (role name, back); scroll body; SafeArea bottom action bar.
Layout:
AppBar "{name}"
[Banner: system role → "System roles can't be edited." (info)] (only if isSystem)
Identity card (AppCard e-1):
name titleLarge
slug mono
description bodyMedium onSurfaceVariant
priority InfoRow "Priority" value
system badge AppBadge System / Custom
"Permissions · {n} of 95" SectionHeader
PermissionGroupSection × N (read-only): group label + granted chips only
[proposed] "Members with this role" — client-joined list (OQ-R7)
Bottom bar: [Edit permissions] FilledButton (hidden if isSystem)
Behaviour:
- Data = role doc from the list fetch (client holds the loaded list; no per-id GET
endpoint exists —
rbac.controller.tsexposes onlyGET /rbac/roles). Deep-linked:id→ load full list, pick by id; missing →AppErrorState404 semantics. - Edit →
/settings/roles/:id/edit(S3). Save → detail refreshes from the returned doc (rbac.service.ts:96returns updated doc). - Members-with-this-role:
(proposed)— computed client-side by intersectingGET /rbac/memberswith the role slug; no server endpoint (OQ-R7).
A11y: group headers Semantics(headers:); chips readable in one pass; contrast for
locked badge ≥ 3:1.
S3 — Role Editor (permission matrix) /settings/roles/new · /settings/roles/:id/edit
The hero. One screen for create and edit; create = same form with empty matrix.
Chrome: AppBar ("Create role" / "Edit role"), subtitle slug; body = form fields
(Section A, 08 §1) + matrix; bottom sticky bar.
Layout (phone portrait):
AppBar "Create role"
name AppTextField (autofocus on create)
slug AppTextField (mono, helper "Used in API & membership")
description AppTextField (multiline, 2)
priority AppTextField (number; default 0, role.schema.ts:82)
───────────── divider
Row: "Permissions" titleMedium • AppBadge "{sel} of 95" (live)
AppSearchBar hint "Search permissions…" (filters perms + groups)
[Matrix] PermissionGroupSection × 26 (scrollable, grouped)
─────────────
Bottom bar: [Cancel] OutlinedButton | [Save role] FilledButton (loading)
Matrix component spec (hero):
- Source of truth:
GET /rbac/permissions(rbac.service.ts:79-81returnsALL_PERMISSIONS,permissions.constants.ts:1-97) fetched on editor open; role's current perms = itspermissions[](role.schema.ts:84-85). - Grouping (forward-looking client matrix): 26 groups derived from the
domainprefix of each permission. Full table (order = source order within group):
| Group | Permissions |
|---|---|
| Organization | organization.read, organization.update, organization.delete, organization.settings.update |
| Users | user.read, user.create, user.update, user.delete, user.import |
| Roles & Access | rbac.role.read, rbac.role.create, rbac.role.update, rbac.role.delete, rbac.member.read, rbac.member.create, rbac.member.update, rbac.member.delete |
| Staff | staff.read, staff.create, staff.update, staff.delete, department.manage, designation.manage |
| Students | student.read, student.create, student.update, student.delete |
| Attendance | attendance.mark, attendance.edit |
| Fees | fees.collect |
| Notifications | notification.read, notification.update |
| CRM | crm.read, crm.lead.manage, crm.campaign.manage |
| Dashboard | dashboard.read, dashboard.widget.manage |
| Reports | report.generate, report.read |
| Biometric | biometric.log.create, biometric.log.read, biometric.device.manage |
| Timetable | timetable.read, timetable.create |
| Houses | houses.read, houses.create, houses.update, houses.delete |
| Rooms | rooms.read, rooms.create, rooms.update, rooms.delete |
| Audit | audit.read |
| Library | books.read, books.create, books.update, books.delete, books.issue, books.return |
| Fines | fines.pay |
| Transport | transport.vehicle.read, transport.vehicle.create, transport.vehicle.update, transport.vehicle.delete, transport.route.read, transport.route.create, transport.route.update, transport.route.delete, transport.driver.read, transport.driver.create, transport.driver.update, transport.driver.delete, transport.assign |
| Settings | settings.read, settings.update, settings.delete |
| Feature Flags | feature-flags.read, feature-flags.update, feature-flags.delete |
| Payments | payments.read, payments.process, payments.refund, payments.reconcile, receipts.read |
| Files | file.read, file.upload, file.delete |
| Webhooks | webhook.create, webhook.read, webhook.update, webhook.delete |
| Search | search |
| Scheduler | scheduler.read, scheduler.create, scheduler.delete |
PermissionGroupSection: group header = label +Checkbox(tri-state: none/some/all)- "{k}/{n}"; body =
WrapofPermissionChips (FilterChip-style, label = full perm string inmono-ish small type, tooltip = humanized name(proposed)).
- "{k}/{n}"; body =
- Toggle semantics: chip tap toggles; group checkbox toggles all in group; search filters to matched group sections (sections with 0 matches collapse to "no results").
- Live count badge updates on every toggle; no server call until Save (one PATCH,
rbac.service.ts:96— no per-cell endpoint exists). - Forward-looking notes: the matrix renders only the 95 permissions returned by the
server; coaching perms (
batch.manageetc.) and roles (batch_coordinator…,docs/IMPLEMENTATION_PLAN.md:713-761) are(planned)and must not be rendered until the server ships them.
Behaviour & rules:
- Create: sends
{name, slug, description?, priority?, isSystem:false, permissions}—isSystem:falseis forced (OQ-R2; server defaulttruelocks the role,role.schema.ts:78-79). - Save →
POST /rbac/rolesorPATCH /rbac/roles/:id; 409 duplicate slug → inline under slug field, focus it; 400 (system role — should be unreachable) → banner. - Dirty state: back navigation with unsaved changes →
AppDialog"Discard changes?". - Empty selection on create → warning banner "Role with no permissions — members will
only see the base shell" (allowed server-side; e.g.
studentrole is[]). - Priority field hint: "Higher number lists the role first" (
rbac.service.ts:76).
States: loading (catalog + role) skeleton; saving → CTA spinner, matrix disabled, anti-double-submit; success → snackbar + pop to detail.
A11y: every chip = toggle with Semantics(checked:) + label + group context;
search results announced (liveRegion "3 groups match"); checkbox tri-state announced
("none / some / all selected").
Tablet/desktop: matrix in a 2-column grid (groups wrap); sticky group header on
scroll; Ctrl+F focuses search; tab order = fields → matrix rows.
S4 — Members List /settings/members
Chrome: AppBar "Members"; headlineMedium; list; FAB.
Layout:
AppBar "Members"
AppSearchBar (name / email filter — merged profile, OQ-R7)
MemberTile × N:
AppAvatar 48 (photo or initials) displayName (titleMedium)
email · joined {date} (bodySmall)
[role chips ×k] (FilterChip, read-only, tap = no-op
w/ tooltip "Managed in role editor")
[status badge: active/suspended/invited] (render-only)
⋮ (Edit roles / Remove — admin only)
FAB "Add member" (extended)
Behaviour:
- Data:
GET /rbac/members(rbac.service.ts:109-111, joinedAt desc) + profile merge viauser.read(batchGET /api/v1/users, OQ-R7); merge failures renderuserIdinmonofallback. - Roles per member =
roles[](organization-member.schema.ts:22) resolved to role docs for labels/priority; unknown slugs (role deleted) render as plain slug chips. - Status badge from
status(organization-member.schema.ts:27); today alwaysactive(OQ-R3);suspended/invitedrender-only until server produces them. - Row menu (admin): "Edit roles" → S5 sheet in edit mode (
PATCH /rbac/members/:id, DTO requiresuserIdbut service ignores it — client may send the sameuserIdfrom the row,rbac.controller.ts:71-72); "Remove member" →AppDialogdestructive confirm ("Remove {name}? Their access ends within 5 minutes.") →DELETE /rbac/members/:id(rbac.service.ts:138-140soft delete; silent 200 even if already removed) → optimistic removal. - Self-guard: Priya removing her own
org_adminmembership → extra typed confirm (type the role slug pattern,05 §5) + warning "You will lose access to this screen immediately on next refresh."
States: skeleton / error / empty ("No members yet — add your first member"); pull to refresh.
S5 — Add Member /settings/members/add (bottom sheet)
Full-height AppBottomSheet (Sp-16 padding, r-xl top):
DragHandle "Add member" titleLarge
Step 1 — Person:
AppSearchBar "Search users…" (debounced, user.read; selectable results = AppListTile:
avatar + displayName + email)
[selected user tile] AppCard: avatar, name, email, [change]
Step 2 — Roles:
SectionHeader "Roles"
FilterChip × roles (GET /rbac/roles, priority order; system + custom; multi-select)
helper: "Roles control permissions. Members can hold several."
[Add member] FilledButton fullWidth (disabled until user + ≥1 role)
Rules (derived):
- Body:
{userId, roles}—@IsMongoId(add-member.dto.ts:6-7),@IsArray@IsString({each})(add-member.dto.ts:9-12). - Duplicate pre-check client-side against loaded member list (unique index
organization-member.schema.ts:48; server would 500 — OQ-R5) → inline error "Already a member". - Success → 200/201 doc (
status:"active",joinedAtnow —rbac.service.ts:122-123); sheet closes, row appears top of S4, snackbar "Member added". - Edit mode (from S4 menu): same sheet, title "Edit roles", user tile read-only, chips preselect current roles, CTA "Save roles".
A11y: sheet traps focus; role chips Semantics(selected:); user results
liveRegion count.
S6 — Permission Audit View /settings/access-audit
Chrome: AppBar "Access audit"; filter bar; paginated list.
Layout:
AppBar "Access audit"
Filter row (Wrap of 3 AppDropdown + clear):
action (free-text suggestions: role.create, member.delete, user.login…)
entityType (roles, organization_members, users…)
actorId (person picker, user.read)
[AppBanner info: "Shows the last 90 days. RBAC writes appear here." (proposed copy)]
AuditRow × 20:
action (labelLarge, monospace accent for RBAC actions)
entityType · actorId (mono)
timestamp (bodySmall, relative)
Infinite scroll + "end of list" footer (meta.totalItems — paginated, 07 §2)
Behaviour:
- Query
GET /audit-logs?page=&limit=&action=&entityType=&actorId=(audit.controller.ts:17-34); default limit 50 (audit.controller.ts:20); pagination envelope withmeta({page,limit,totalItems,totalPages,hasNext,hasPrevious},07 §2). - RBAC focus: presets in the action dropdown (
(proposed)convenience): "Role created", "Role updated", "Role deleted", "Member added", "Member updated", "Member removed" — mapped to whateveractionstrings the audit module writes (audit-writequeue,PLAN.md:189-191; exact strings(planned), OQ-R8). - Row tap → no detail screen (no server detail endpoint) — copies requestId
(
(proposed)). - 401/403 handling standard; server currently gates with
JwtAuthGuardonly (audit.controller.ts:9) — client still requiresaudit.read(mismatch flagged, Phase-5(planned)).
Tablet/desktop: filters in the AppBar row; table view (columns: time, action, entity, actor, requestId) instead of list tiles.