critical priority low complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

Authenticated user with global_admin role navigating to any non-allowlisted route is redirected to /no-access
Authenticated user with global_admin role navigating to /logout is allowed through — redirect returns null
Authenticated user with global_admin role navigating to /auth or any /auth/* sub-route is allowed through
Authenticated user with non-blocked role (coordinator, peer_mentor, org_admin) navigating anywhere is not intercepted by this guard
Unauthenticated user is not intercepted by this guard (authentication guard runs first in the chain)
Redirect to /no-access does not create a navigation loop — guard detects it is already on /no-access and returns null
GoRouter router.refresh() is called whenever isRoleBlockedProvider emits a new value, ensuring the redirect is re-evaluated reactively
Guard does not call async operations inside the redirect callback — role state is read synchronously from cached Riverpod state
Integration test: simulate global_admin login → verify final route is /no-access; simulate coordinator login → verify final route is /home

Technical Requirements

frameworks
Flutter
GoRouter (redirect callback, RouterNotifier pattern)
Riverpod (ref.read for synchronous role state access)
apis
isRoleBlockedProvider (StreamProvider<bool> from task-004)
GoRouter.redirect callback signature: String? Function(BuildContext, GoRouterState)
data models
GoRouterState (location, subloc)
NoAccessRouteConstants (allowlisted paths from task-006)
performance requirements
Redirect callback must be synchronous (return String? not Future<String?>) — GoRouter does not support async redirect
Role state read via ref.read() — not ref.watch() — inside redirect to avoid re-entrant rebuilds
security requirements
Guard must be stateless and pure — same input state must always produce the same redirect output
Guard must handle null role state as blocked (fail-secure) — if role is not yet loaded, redirect to /no-access until confirmed non-blocked
Guard must not be bypassable by direct URL manipulation on web — server-side RLS is the true security boundary, but client guard must be consistent

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use the RouterNotifier pattern from Riverpod + GoRouter: create a NoAccessRouterNotifier extends Notifier that listens to isRoleBlockedProvider and calls ref.notifyListeners() (or uses a ChangeNotifier) to trigger router.refresh(). In the GoRouter constructor: refreshListenable: ref.watch(noAccessRouterNotifierProvider), redirect: (context, state) { final isBlocked = ref.read(isRoleBlockedProvider).valueOrNull ?? true; if (!isBlocked) return null; final allowlist = NoAccessRouteConstants.allowlistedPaths; if (allowlist.any((p) => state.location.startsWith(p))) return null; if (state.location == '/no-access') return null; return '/no-access'; }. The ??

true default ensures fail-secure behavior during loading. Keep the redirect callback a pure function — extract the allowlist check into a separate static method for testability.

Testing Requirements

Integration tests using flutter_test with a ProviderScope wrapping a GoRouter test harness. Test scenarios: (1) global_admin role → initial route redirects to /no-access, (2) global_admin navigates to /logout → allowed, (3) global_admin navigates to /auth/login → allowed, (4) coordinator role → no redirect applied, (5) role changes from coordinator to global_admin mid-session → router.refresh() triggers and next navigation goes to /no-access, (6) already on /no-access with global_admin → no redirect loop (returns null). Use pumpWidget with MaterialApp.router and verify tester.routeInformationProvider.value.location.

Component
No-Access Route Guard
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.