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

15 — Flutter Implementation Guide (RBAC Module)

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.builder of group sections — never a Column of 95 chips (build cost would blow the 8 ms frame budget on scroll).
  • RepaintBoundary per PermissionGroupSection — toggling a chip repaints one section, not the whole matrix.
  • Const everywhere: chips are const-constructible (label, selection state as Set<String> membership check) → FilterChip inside Wrap; Wrap is fine per group (≤ 13 chips, transport.* max) but the matrix must not be one giant Wrap.
  • Search: precomputed group→matches index; filter collapses sections without touching the list (no rebuild of chips).
  • State: selected as Set<String> (hash membership O(1)); group tri-state computed in didUpdateWidget-style recalculation, memoized per group.
  • Save payload: single List<String>jsonEncode of 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}' (mirrors rbac.service.ts:48) + Hive persistence per tenant (key perm_mirror:{tenantId}), shared_preferences too 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.permissionsChanged stream → AppRouter.refresh() (05 §9); rebuild also on AuthCubit auth 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

  • RbacRepository methods: listRoles(), listPermissions(), createRole(dto), updateRole(id, dto), deleteRole(id), listMembers(), addMember(dto), updateMemberRoles(id, dto), plus queryAudit(filters) (audit-logs via 00-shared/11 §5 AppDio).
  • Envelope mapping → typed ApiException(code, status, message); 409 → Duplicate surfaced inline; 500-on-duplicate-member pre-checked (08 §2).
  • Idempotency-Key header on POST /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 from permissions.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)