high priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

A top-level accessDenialServiceProvider (Provider<AccessDenialService>) is defined and accessible app-wide
A top-level isRoleBlockedProvider (StreamProvider<bool>) is defined and emits the service's isRoleBlocked stream
A top-level adminPortalUrlProvider (FutureProvider<String>) is defined and resolves via the service's getAdminPortalUrl()
All three providers are defined in a single dedicated file (e.g., access_denial_providers.dart) — not scattered across feature files
The service provider correctly reads its dependencies (RBAC provider, config repository provider) from the Riverpod ref, not via direct instantiation
isRoleBlockedProvider.when(data:, loading:, error:) works correctly in widget tests — loading state emitted initially, data state follows
Providers are registered under the root ProviderScope in main.dart (or equivalent app entry point)
No circular dependencies introduced in the provider graph — verified by running the app without ProviderScope override errors
Widget test confirms a consumer widget rebuilds when isRoleBlockedProvider emits a new value

Technical Requirements

frameworks
Flutter
Riverpod (StreamProvider, FutureProvider, Provider)
apis
AccessDenialService.isRoleBlocked (Stream<bool>)
AccessDenialService.getAdminPortalUrl() (Future<String>)
performance requirements
StreamProvider must use autoDispose if the no-access screen is not always active — prevents dangling subscriptions
FutureProvider result should be cached by Riverpod's default keep-alive — no manual cache needed at provider layer
security requirements
Provider graph must not expose the raw AccessDenialService instance to widgets — only the derived StreamProvider and FutureProvider should be consumed by UI

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Create access_denial_providers.dart with three providers. Use ref.watch(rbacRoleProvider) inside accessDenialServiceProvider to get the role source, and ref.watch(noAccessConfigRepositoryProvider) for the config repo — this makes dependencies explicit and testable. For the StreamProvider: final isRoleBlockedProvider = StreamProvider.autoDispose((ref) { final service = ref.watch(accessDenialServiceProvider); return service.isRoleBlocked; });. For the FutureProvider: final adminPortalUrlProvider = FutureProvider.autoDispose((ref) { final service = ref.watch(accessDenialServiceProvider); return service.getAdminPortalUrl(); });.

Use autoDispose on derived providers to avoid memory leaks when the no-access screen is not mounted. The route guard (task-005) will use ref.read(isRoleBlockedProvider) in the redirect callback — document this pattern in the provider file's doc comment.

Testing Requirements

Widget tests using flutter_test and riverpod's ProviderContainer for unit-level provider tests. Test scenarios: (1) isRoleBlockedProvider emits AsyncLoading then AsyncData(true) when service stream emits true, (2) adminPortalUrlProvider resolves to expected URL string, (3) provider override in ProviderScope works correctly for mocking in widget tests. Use ProviderContainer directly for non-widget provider tests. Verify no StateError thrown on provider disposal.

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.