08 — Form Specifications (RBAC Module)
- 1. Create / Edit Role form (S3 top section)
- 2. Add Member form (S5 sheet)
- 3. Audit filter form (S6, non-mandatory)
- 4. Validation summary table
Field-by-field specs for the three RBAC forms. Validation mirrors the server DTOs (
create-role.dto.ts,add-member.dto.ts) plus client-side guards the server does not enforce (flagged per field). Wire contract per 00-shared/07.
1. Create / Edit Role form (S3 top section)
Request shape: CreateRoleDto (create-role.dto.ts:10-38) — used for both POST /rbac/roles and PATCH /rbac/roles/:id (rbac.controller.ts:39-49).
| # | Field | Control | Server validation (source) | Client rules |
|---|---|---|---|---|
| 1 | name | AppTextField, autofocus (create) | @IsString required (create-role.dto.ts:12-13) | required; max 60 chars; trim; unique-ish by display (slug is the real unique) |
| 2 | slug | AppTextField mono | @IsString required (create-role.dto.ts:15-17) | required; pattern ^[a-z0-9_]{2,40}$; generated suggestion from name (snake_case) editable; on 409 DUPLICATE_RESOURCE (rbac.service.ts:86-87) → inline error + focus; helper "Used in API & membership — can't be changed later" (update actually allows $set of slug, rbac.service.ts:96 — but changing breaks member role arrays; treat as immutable in UI) |
| 3 | description | AppTextField multiline | @IsOptional @IsString (create-role.dto.ts:19-22) | optional; ≤ 200 chars; helper text; 2 lines |
| 4 | priority | AppTextField number | @IsOptional @IsNumber (create-role.dto.ts:29-32); schema default 0 (role.schema.ts:82) | integer 0–1000; default 0; helper "Higher = listed first (Roles list sorts priority desc, rbac.service.ts:76). Display only — doesn't grant more power." |
| 5 | isSystem | hidden (never rendered) | @IsOptional @IsBoolean (create-role.dto.ts:24-27); schema default true (role.schema.ts:78-79) | always sends false (OQ-R2) — otherwise the new role locks itself |
| 6 | permissions | PermissionMatrix (07 §1) | @IsOptional @IsArray (create-role.dto.ts:34-37) — no whitelist check | client-side only: every string must be in the catalog from GET /rbac/permissions (rbac.service.ts:79-81); empty set allowed (warning banner, e.g. student role) |
Submit flow (both modes):
- Edit:
PATCH /rbac/roles/:idwith the full doc{name, slug?, description?, priority?, isSystem?, permissions}— server$sets wholesale (rbac.service.ts:96); system roles → 400 "Cannot modify system roles." (rbac.service.ts:94-95) — unreachable via UI (editor hidden for system roles). - Create:
POST /rbac/roles; duplicate slug → 409 (rbac.service.ts:87). - Errors: field-level for slug/name (400
VALIDATION_ERRORwithdetails[].field), banner for 403; anti-double-submit; success → snackbar → pop to detail.
2. Add Member form (S5 sheet)
Request shape: AddMemberDto (add-member.dto.ts:4-12) for POST /rbac/members;
same DTO reused by PATCH /rbac/members/:id (edit mode — rbac.controller.ts:71-72).
| # | Field | Control | Server validation (source) | Client rules |
|---|---|---|---|---|
| 1 | userId | UserSearchPicker (07 §9), read-only tile once picked | @IsMongoId required (add-member.dto.ts:6-7) | required; must exist in user.read results; pre-check against loaded members → "Already a member" inline (server 500s on E11000, OQ-R5) |
| 2 | roles | RolePickerChips (07 §8) multi-select | @IsArray @IsString({each: true}) required (add-member.dto.ts:9-12) | ≥ 1 required for submit; ≤ 10 (UI cap, (proposed)); slugs must resolve in GET /rbac/roles (warn + strip unknown); system roles selectable (assign teacher is normal) |
Submit flow:
- Create:
POST /rbac/members→ 200/201 doc withstatus:"active",joinedAtserver-set (rbac.service.ts:122-123); sheet closes; row appears top (list sorts joinedAt desc,rbac.service.ts:110). - Edit mode:
PATCH /rbac/members/:idwith{userId: <row's userId>, roles:[…]}(server ignoresuserId—rbac.controller.ts:72; client echoes the row's value for DTO compliance); success → tile chips refresh; 404 → "Member removed elsewhere" snackbar + remove row. - Remove:
AppDialogdestructive confirm →DELETE /rbac/members/:id(rbac.service.ts:138-140); silent 200 even if already gone (no 404) → optimistic removal.
3. Audit filter form (S6, non-mandatory)
| Field | Control | Query param (audit.controller.ts:20-25) | Notes |
|---|---|---|---|
| action | AppDropdown (free-text) | action | suggestions + RBAC presets (proposed) (OQ-R8) |
| entityType | AppDropdown (free-text) | entityType | e.g. roles, organization_members |
| actorId | UserSearchPicker | actorId | via user.read |
| page/limit | implicit | page, limit (default 50 server, audit.controller.ts:20) | infinite scroll appends pages |
Filters combine (all optional; server builds filter object audit.controller.ts:26-29);
"Clear" resets all.
4. Validation summary table
| Rule | Where enforced |
|---|---|
| slug unique per tenant | server 409 + unique index (rbac.service.ts:86-87, role.schema.ts:89) + client inline |
| system role immutable | server 400 (rbac.service.ts:94-95,104-105) + UI hides editor/delete |
| isSystem forced false on create | client only (OQ-R2) |
| permission names whitelisted | client only (server @IsArray, create-role.dto.ts:36) |
| member duplicate | client pre-check (server 500, OQ-R5) |
| member roles non-empty | client only (server @IsArray allows [], add-member.dto.ts:10) |
userId MongoId | server 400 (add-member.dto.ts:6) |