critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

An abstract Dart class (or interface via abstract class) named AccessDenialService is created in the correct service layer directory
The interface exposes isRoleBlocked as a Stream<bool> or Riverpod AsyncNotifier<bool> that emits true when the current user's role is in the blocked set
The interface exposes getAdminPortalUrl() returning Future<String?> (nullable to handle orgs without a portal URL)
The interface exposes blockedRoles as a Set<String> or Set<UserRole> (using the project's existing role enum/type)
All method signatures are documented with Dart doc comments explaining the contract, including what constitutes a 'blocked role' in this system
The interface includes a factory constructor or static create() method signature pattern consistent with the project's DI/service registration approach
A corresponding Riverpod provider declaration (abstract or concrete stub) is defined so consumers can reference the provider type immediately
The file compiles cleanly with `npm run build` (dart compilation) and introduces no new dependencies
The interface is reviewed against the no-access screen requirements: global admin role block is the primary use case per the app architecture spec

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase (role data source — interface only, no implementation in this task)
data models
UserRole (existing role enum: admin, coordinator, peer_mentor — or equivalent project type)
AdminPortalUrl (String, nullable, per-organization)
performance requirements
isRoleBlocked stream must emit within 500ms of role state changes to avoid delayed access denial
security requirements
blockedRoles set must be immutable (UnmodifiableSetView or final) to prevent accidental mutation by consumers
Admin portal URL must never be inferred from client-side logic — it must come from a server-side source (Supabase) to prevent URL spoofing
The interface must not expose raw user PII — only the boolean block status and the portal URL

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

This is a contract-definition task — no business logic is implemented here. Place the file at lib/services/access_denial_service.dart (or equivalent project service path). Use Dart's abstract class pattern for the interface. Define isRoleBlocked as a Stream rather than a Future because role state can change mid-session (e.g., an admin revokes access remotely) and consumers need to react reactively.

Define blockedRoles as UnmodifiableSetView or as a const Set if roles are static. For the Riverpod provider, define a Provider at the bottom of the file using throw UnimplementedError() as placeholder — this allows consumers to import and reference the provider type immediately while the implementation task is pending. Follow the existing project service interface pattern exactly (check similar services like AuthService or RoleService for naming and structural conventions).

Testing Requirements

No runtime tests needed for an abstract interface definition. Write one unit test file that creates a mock implementation of AccessDenialService (using Mockito or manual stub) to verify the interface is implementable and all methods are callable as specified. Assert that a mock returning isRoleBlocked = Stream.value(true) and getAdminPortalUrl() = Future.value('https://...') compiles and is assignable to AccessDenialService. This ensures the interface is sound before any real implementation is built.

Component
Access Denial Service
service low
Epic Risks (2)
high impact medium prob technical

If the GoRouter redirect callback evaluates the no-access route itself as a blocked destination, it will trigger an infinite redirect loop, crashing the navigator.

Mitigation & Contingency

Mitigation: Add an explicit guard condition in the redirect callback: return null (no redirect) when the current location is already the no-access route or the logout route. Write a dedicated unit test covering this exact scenario.

Contingency: If the redirect loop is detected in production, deploy a hotfix that adds the null-return guard; the feature can be toggled off via the existing feature-flag infrastructure while the fix is prepared.

medium impact low prob integration

The access-denial-service may read role state before authentication completes (e.g. during app resume), causing a temporary false-positive block that redirects valid peer-mentor users to the no-access screen.

Mitigation & Contingency

Mitigation: Subscribe to the role-state-manager's loading/ready lifecycle and only evaluate role-based access once the RBAC state is confirmed as loaded. Return a 'pending' state that causes the guard to defer rather than redirect.

Contingency: Add a retry mechanism: if a user lands on the no-access screen but their role subsequently resolves as non-blocked, automatically navigate them to the role-based home screen.