critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

RoleAccessValidator class implemented in lib/features/stats/domain/validators/role_access_validator.dart
Typed exception class StatsAccessDeniedException defined with fields: requestedScope (String), callerRole (UserRole), callerId (String) — message must not include sensitive IDs in production builds
validateCoordinatorScope(coordinatorId, chapterId, callerContext) throws StatsAccessDeniedException when callerContext.role is UserRole.coordinator and callerContext.chapterId != chapterId
validateCoordinatorScope passes (does not throw) when callerContext.role is UserRole.coordinator and callerContext.chapterId == chapterId
validateCoordinatorScope passes for UserRole.orgAdmin when callerContext.orgId matches the org that owns the requested chapterId
validateOrgScope(orgId, callerContext) throws StatsAccessDeniedException when callerContext.role is UserRole.coordinator (coordinators cannot query org-wide data)
validateOrgScope passes for UserRole.orgAdmin when callerContext.orgId == orgId
Both validate methods are synchronous — no async calls, no Supabase queries
RoleAccessValidator is injectable (constructor accepts no external dependencies — all logic is pure)
Public API is documented with Dart doc comments explaining the defence-in-depth rationale
No dependency on BLoC, Riverpod, or any Flutter widget layer — pure Dart domain class

Technical Requirements

frameworks
Dart
data models
UserRole
CallerContext (carries role, orgId, chapterId, userId)
performance requirements
Both validate methods must be O(1) — no database lookups, no list iterations
security requirements
StatsAccessDeniedException message must use kDebugMode-gated detail: full context in debug, generic 'Access denied' in release builds
Validator must be called before every repository query in the stats service layer — never bypass for 'convenience'
Class must be final (cannot be subclassed) to prevent accidental override of security logic

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define a CallerContext value object (immutable, with == and hashCode) to carry the caller's identity and role instead of passing individual parameters — this makes the API harder to misuse and easier to extend. Use a sealed class or enum for UserRole to ensure exhaustive switch coverage. The class should be final to prevent subclassing. For the release-mode message sanitisation, use a static helper _buildMessage(bool isDebug, ...) that takes kDebugMode as a parameter — this makes it testable without relying on the global constant.

Document explicitly in the class header that this validator is a second line of defence and that Supabase RLS is the primary enforcement mechanism, so reviewers understand the layered security model.

Testing Requirements

Unit tests with flutter_test. No mocks required — RoleAccessValidator is pure. Test matrix: coordinator role + matching chapter (pass), coordinator role + wrong chapter (throw), coordinator role + org scope request (throw), org-admin role + matching org (pass), org-admin role + wrong org (throw), org-admin role + coordinator scope in own org (pass). Verify that StatsAccessDeniedException carries the expected callerRole and requestedScope fields.

Verify that the exception message in release mode (kReleaseMode) does not contain the callerId or chapterId. Aim for 100% branch coverage on the validator.

Component
Role Access Validator
service low
Epic Risks (3)
high impact medium prob technical

Pre-aggregated Supabase views may still be slow for orgs with very large activity datasets (NHF with 1,400 chapters). If the view query plan performs sequential scans, dashboard load times could exceed acceptable thresholds and degrade the perceived value of the feature.

Mitigation & Contingency

Mitigation: Design views with composite indexes on (org_id, coordinator_id, month) from the start. Run EXPLAIN ANALYZE during development against a seeded dataset of realistic scale. Add materialized view refresh strategy if needed.

Contingency: If live view performance is insufficient, convert to materialized views refreshed on a schedule or on activity-write triggers. Expose the refresh delay transparently in the UI with a 'last updated' timestamp.

high impact low prob security

Supabase RLS policies for the stats views may not be configured correctly during initial migration, potentially allowing cross-coordinator data leakage before the RoleAccessValidator layer is reached. This is a security and compliance risk.

Mitigation & Contingency

Mitigation: Write RLS integration tests as part of this epic that explicitly verify a coordinator JWT cannot read another coordinator's stats rows. Apply RLS policies in the migration script itself, not as a manual step.

Contingency: If an RLS gap is discovered post-deployment, immediately disable the stats screen via a feature flag, apply the corrected RLS migration, and re-enable after verification. Log and audit all queries that ran during the gap window.

medium impact medium prob integration

Cache invalidation logic may not be triggered correctly when a new activity is registered by a peer mentor or when an expense approval is granted. Stale data could cause coordinators to make decisions based on outdated KPIs, undermining trust in the dashboard.

Mitigation & Contingency

Mitigation: Define explicit invalidation event contracts with the activity registration and expense approval pipelines. Implement an event bus subscription within StatsCacheManager. Document the invalidation contract in code.

Contingency: If event-driven invalidation proves unreliable, add a manual 'Refresh' pull-to-refresh gesture on the dashboard and reduce TTL to 5 minutes as a fallback degradation strategy.