15 — Flutter Implementation Guide (RBAC Module)
- 1. Folder structure
- 2. PermissionMatrix — performance spec (hero)
- 3. Permission-mirror caching (
13 §2) - 4. Route guards (
go_router) - 5. Repository & networking
- 6. i18n & error copy
- 7. Tests
- 8. Performance checklist (RBAC-specific)
Implementation guidance on top of 00-shared/11. Forward-looking: no client repo yet. Focus areas: matrix widget performance, permission-mirror caching, route guards, plus module folder structure, DTOs, and tests.
1. Folder structure
lib/features/rbac/
├── data/
│ ├── dto/role_dto.dart # RoleDto.fromJson (role.schema.ts:67-86 shape)
│ ├── dto/member_dto.dart # MemberDto.fromJson (organization-member.schema.ts:14-44)
│ ├── dto/permission_dto.dart # plain String list (GET /rbac/permissions)
│ ├── models/role.dart # domain: name, slug, isSystem, priority, Set<String> permissions
│ ├── models/member_view.dart # member + merged UserProfile (OQ-R7)
│ └── repositories/
│ ├── rbac_repository.dart # roles + members + catalog calls
│ └── permission_mirror.dart # client-side permission cache (13 §2)
├── domain/
│ └── rbac_permissions.dart # group mapping (26 groups, 06 §3) + humanized labels (i18n)
└── presentation/
├── cubit/role_list_cubit.dart / role_editor_cubit.dart /
│ member_list_cubit.dart / member_sheet_cubit.dart / audit_cubit.dart
├── pages/roles_page.dart / role_detail_page.dart / role_editor_page.dart /
│ members_page.dart / access_audit_page.dart / permission_denied_page.dart
└── widgets/ permission_matrix.dart / permission_group_section.dart /
permission_chip.dart / role_row.dart / member_tile.dart /
role_picker_chips.dart / user_search_picker.dart / audit_row.dart
DTOs map envelope payloads (00-shared/11 §4); widgets consume models only.
2. PermissionMatrix — performance spec (hero)
The requirement: 95 chips in 26 groups, instant toggle, 60 fps scroll on a
mid-range phone (00-shared/10 §1).
- Lazy build: one
ListView.builderof group sections — never aColumnof 95 chips (build cost would blow the 8 ms frame budget on scroll). RepaintBoundaryperPermissionGroupSection— toggling a chip repaints one section, not the whole matrix.- Const everywhere: chips are
const-constructible (label, selection state asSet<String>membership check) →FilterChipinsideWrap;Wrapis fine per group (≤ 13 chips,transport.*max) but the matrix must not be one giantWrap. - Search: precomputed group→matches index; filter collapses sections without touching the list (no rebuild of chips).
- State:
selectedasSet<String>(hash membership O(1)); group tri-state computed indidUpdateWidget-style recalculation, memoized per group. - Save payload: single
List<String>—jsonEncodeof the set at Save only. - Golden tests: 3 sizes × light/dark × editable/readOnly with the full 95-perm fixture; profile frame-build < 16 ms on reference device.
(planned)coaching perms (docs/IMPLEMENTATION_PLAN.md:751-761) render only when the server catalog includes them — group list must tolerate unknown prefixes (fallback group "Other").
3. Permission-mirror caching (13 §2)
- In-memory
Map<String, List<String>>keyed'sl:{tenantId}:perm:{userId}'(mirrorsrbac.service.ts:48) + Hive persistence per tenant (keyperm_mirror:{tenantId}),shared_preferencestoo small for 95-string lists. - Stale-while-revalidate: serve cached instantly; background refetch; update on
success; TTL ≤ 300 s aligned to server EX 300 (
rbac.service.ts:68). - Invalidate on: login/logout, own RBAC writes (role save/delete, member add/edit/remove), manual "Refresh session".
- No mirror → gate open? No: on cold start with empty mirror, routes resolve
optimistically against cached JWT roles; actual enforcement is server-side. 403s
surface through the error pipeline (
00-shared/06 §5) and the mirror re-syncs. - Never ship the 95 names as a hardcoded client constant — catalog must come from
GET /rbac/permissions(rbac.service.ts:79-81); mirror is cache, catalog is data.
4. Route guards (go_router)
final rbacRoutes = [
GoRoute(path: '/settings/roles', redirect: _rbacGuard('rbac.role.read'), ...),
GoRoute(path: '/settings/members', redirect: _rbacGuard('rbac.member.read'), ...),
GoRoute(path: '/settings/access-audit', redirect: _rbacGuard('audit.read'), ...),
];
String? _rbacGuard(String perm) => (context) {
final auth = di<AuthCubit>().state;
final mirror = di<PermissionMirror>();
final ok = auth.roles.contains('org_admin') // server reality (rbac.controller.ts:21)
|| mirror.has(auth.tenantId, auth.userId, perm); // intended perm model (OQ-R1)
return ok ? null : '/settings/403';
};
- Role-or-permission composition per
04 §3/13 §4— covers both the current role-gated server and the perm-gated future without a client rewrite. - Route rebuild on change:
PermissionMirror.permissionsChangedstream →AppRouter.refresh()(05 §9); rebuild also onAuthCubitauth change (re-login with new roles claim). - Deep-link to removed route → 403 screen (never blank); session-expiry overlay wins
over 403 (401 takes precedence,
00-shared/06 §3.6).
5. Repository & networking
RbacRepositorymethods:listRoles(),listPermissions(),createRole(dto),updateRole(id, dto),deleteRole(id),listMembers(),addMember(dto),updateMemberRoles(id, dto), plusqueryAudit(filters)(audit-logs via00-shared/11 §5AppDio).- Envelope mapping → typed
ApiException(code, status, message); 409 →Duplicatesurfaced inline; 500-on-duplicate-member pre-checked (08 §2). Idempotency-Keyheader onPOST /rbac/members(write-once op,00-shared/07 §9).
6. i18n & error copy
- Keys:
rbac.title,rbac.role.create,rbac.matrix.count("{sel}","{total}"),rbac.system.role.locked,rbac.propagation.note,rbac.self.remove.confirm,perm.{group}.{name}(humanized labels, fallback raw string), 403 screen copy. - Server messages (e.g. "Cannot modify system roles.") rendered only as fallback for
business 4xx (
00-shared/07 §11).
7. Tests
- Unit: mirror TTL/invalidate; matrix tri-state; slug validator
^[a-z0-9_]{2,40}$; group mapping of all 95 perms (fixture frompermissions.constants.ts:1-97). - Widget: S1–S6 × {loading, error, empty, content}; matrix golden (3 sizes, dark/light); 403 page.
- Integration: J1 create-role → assign → member add → re-login (J4); guard-denied (J5) with mocked 403 envelope.
- E2E: tenant isolation with two tenants (T1–T6); permission propagation ≤ 5 min (M4/R4).
8. Performance checklist (RBAC-specific)
- Matrix first frame < 300 ms with cached catalog; scroll 60 fps profiled
- No rebuild of all 95 chips on single toggle (RepaintBoundary verified)
- Mirror reads synchronous from memory (no disk read on hot path)
- 95-perm PATCH serialization < 10 ms
-
Memory: no growth across 20 min matrix session (
00-shared/10 §1)