high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A user with role `peer_mentor` calling any public method on ReportHistoryService receives an `AccessDeniedException` (or equivalent typed exception) — no data is returned
A user with role `coordinator` calling any public method succeeds (assuming valid org scope and data)
A user with role `org_admin` calling any public method succeeds
Role check occurs before any repository call — verified by confirming the mock repository is never called when role is unauthorized (using Mockito `verifyNever`)
The role check is implemented once (single location) and applied via a shared guard method or mixin — not duplicated per method
Adding a new public method to the service without calling the guard causes a failing test (guard-coverage test)
Unauthenticated users (no session) receive `AuthenticationException`, not `AccessDeniedException`
The set of permitted roles (`coordinator`, `org_admin`) is defined as a constant or enum — not as inline string literals
Dart analyzer reports zero errors/warnings on the updated service file
Unit tests cover all role scenarios (peer_mentor denied, coordinator allowed, org_admin allowed, unauthenticated rejected)

Technical Requirements

frameworks
Flutter
Riverpod (auth/role state injection)
supabase_flutter (session/role access)
apis
AuthRepository or equivalent — exposes current user role
ReportHistoryService (all public methods must be guarded)
data models
UserRole (enum: peer_mentor, coordinator, org_admin — align with existing project role definitions)
AuthState / UserSession (role source)
performance requirements
Role check adds less than 1ms overhead per method call (pure in-memory comparison)
security requirements
Role must be sourced from the server-validated auth session — never from a client-supplied parameter
The RLS policies on the database provide a second enforcement layer; the service-layer check is a defense-in-depth measure and must not be removed on the assumption that RLS is sufficient
Failing the role check must never leak information about whether records exist (throw AccessDeniedException, not 'no records found for your role')

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement role checking as a private guard method: `void _requireHistoryAccess() { final role = _authRepository.currentRole; if (role == null) throw AuthenticationException(); if (!_allowedRoles.contains(role)) throw AccessDeniedException('Report history requires coordinator or admin role'); }`. Call `_requireHistoryAccess()` as the first line of every public method. Define `static const _allowedRoles = {UserRole.coordinator, UserRole.orgAdmin}` at the top of the class. This single-point-of-definition makes it trivial to audit and extend.

Align `UserRole` enum values with those defined elsewhere in the project — check `lib/core/enums/` or equivalent for the canonical definition. Do not create a duplicate enum. If the project uses string-based roles from Supabase JWT claims, map them to the enum at the repository/auth layer boundary, so the service always works with typed enums.

Testing Requirements

Unit tests using Mockito mocks. Test matrix (role × method): peer_mentor × fetchHistory → AccessDeniedException, peer_mentor × any other public method → AccessDeniedException, coordinator × fetchHistory → success (delegates to repository mock), org_admin × fetchHistory → success. Additional tests: unauthenticated → AuthenticationException; repository mock is never invoked when role is unauthorized (`verifyNever(mockRepository.fetchPage(...))`). Implement a 'guard coverage' test that uses reflection or an explicit list to verify every public method name in `ReportHistoryService` appears in the test file's authorized-call test cases — flags new methods added without test coverage.

Component
Report History Service
service low
Epic Risks (3)
high impact medium prob dependency

The ReportReexportCoordinator must invoke the Bufdir export pipeline defined in the bufdir-report-export feature. If that feature's internal API changes (renamed services, altered parameters), the re-export coordinator will break silently at runtime.

Mitigation & Contingency

Mitigation: Define a stable, versioned interface (abstract class or Dart interface) for the export pipeline entry point. The re-export coordinator depends only on this interface, not on concrete export service internals. Document the contract in both features.

Contingency: If the export pipeline breaks the re-export coordinator, fall back to surfacing a clear 'regeneration unavailable' message to the coordinator with instructions to use the primary export screen for the same period as a workaround, while the interface mismatch is fixed.

high impact low prob security

The audit trail must be immutable — coordinators must not be able to edit or delete past events. If the RLS policies allow UPDATE or DELETE on audit event rows, a coordinator could suppress evidence of a re-export or failed submission.

Mitigation & Contingency

Mitigation: Apply INSERT-only RLS policies to the audit events table (no UPDATE, no DELETE for any non-service-role user). Use a separate service-role key for writing audit events, never the user's JWT. Validate this in integration tests by asserting that UPDATE and DELETE calls from coordinator-role sessions are rejected with RLS errors.

Contingency: If immutability is compromised before detection, run a database audit comparing the audit log against the main history table timestamps to identify tampered records, restore from backup if needed, and issue a patch RLS migration immediately.

low impact low prob technical

The user stories require filter state (year, period type, status) to persist within a session so coordinators do not lose context when navigating away. Implementing this with Riverpod state management could cause stale filter state if the provider is not properly scoped to the session lifecycle.

Mitigation & Contingency

Mitigation: Scope the filter state provider to the router's history route scope, not globally. Use autoDispose with a keepAlive flag tied to the session so filters reset on logout but persist on tab switches within the same session.

Contingency: If filter state becomes stale or leaks between sessions, add an explicit reset in the logout handler that disposes all scoped providers. This is a UX degradation (coordinator must re-apply filters) rather than a data integrity issue.