critical priority medium complexity infrastructure pending infrastructure specialist Tier 5

Acceptance Criteria

Global admin role (AppRole.globalAdmin) is detected by the guard and always redirected to '/no-access', regardless of the target route
The '/no-access' route itself does NOT redirect again (guard allows it through for all roles to prevent infinite redirect loop)
The login route ('/login') does NOT redirect again when user is unauthenticated (prevents infinite redirect loop)
Redirect fires correctly on deep link open: app launched with a deep link to any route while session has global_admin role → lands on No-Access Screen
Redirect fires correctly on app resume: app backgrounded with global_admin session, brought to foreground, navigates to any route → redirect to No-Access Screen
Redirect fires correctly on standard programmatic navigation: GoRouter.go('/home') with global_admin role → lands on No-Access Screen
No-Access Screen displays a clear, accessible message explaining why access is blocked and offers a logout or contact-support action
Unit tests pass for all 4 scenarios: global_admin redirect, coordinator access, peer_mentor access, unauthenticated access
Guard contains no side effects (no SnackBar, no analytics calls) — pure redirect logic only
Refreshable guard: when a global_admin session is cleared (logout) and a normal-role session begins, the guard no longer redirects to No-Access

Technical Requirements

frameworks
Flutter
GoRouter
BLoC
Riverpod
apis
Supabase auth session (to detect role on app resume via onAuthStateChange)
data models
AppRole enum (must include globalAdmin value)
ActiveRoleState
AuthState
performance requirements
Global admin detection is an O(1) enum comparison — no performance concern
Guard refresh on app resume must complete before first frame is rendered to prevent flash of restricted content
security requirements
Global admin detection must use the role from the Supabase JWT claims or server-confirmed session — not a local flag
The '/no-access' route must not expose any application data or navigation options beyond logout
ui components
No-Access Screen with clear message, role context display, logout button
Semantics wrapper on No-Access Screen message for screen reader accessibility

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Extract the guard logic into a pure static method RoleRouteGuard.evaluate(AppRole? role, String targetPath) → String? for easy unit testing without Flutter widget context. The GoRouter redirect callback simply calls this method.

To handle app resume correctly, pass a refreshListenable to GoRouter that listens to both the auth state stream (Supabase onAuthStateChange) and the role state stream — any emission triggers guard re-evaluation. Use an enum guard clause: if (role == AppRole.globalAdmin && path != '/no-access') return '/no-access'. Always check for exempt routes (login, no-access, splash) before applying role checks to prevent infinite redirect loops. The No-Access Screen should call Supabase.instance.client.auth.signOut() on the logout button tap and navigate to '/login'.

Testing Requirements

Unit tests (flutter_test, mocktail for mocking): (1) RoleRouteGuard.evaluate(AppRole.globalAdmin, '/home') == '/no-access', (2) RoleRouteGuard.evaluate(AppRole.globalAdmin, '/no-access') == null (no loop), (3) RoleRouteGuard.evaluate(null, '/home') == '/login', (4) RoleRouteGuard.evaluate(AppRole.coordinator, '/home') == null, (5) RoleRouteGuard.evaluate(AppRole.peerMentor, '/coordinator-only') == '/home'. Widget test: render No-Access Screen and assert message text and logout button are present and accessible. Integration test: configure full GoRouter with guard, set globalAdmin role in provider, navigate to '/home', assert route is '/no-access'. Test app resume scenario by simulating GoRouter refreshListenable notification with globalAdmin role.

Target 100% line coverage on guard redirect method.

Component
Role-Based Route Guard
infrastructure medium
Epic Risks (3)
high impact medium prob technical

Combining GoRouter's declarative redirect logic in the route guard with StatefulShellRoute's stateful branch management is known to produce subtle bugs where the shell rebuilds unnecessarily on role switches, losing tab state or causing double-navigation events.

Mitigation & Contingency

Mitigation: Implement the route guard as a GoRouter redirect callback that only evaluates role from an already-resolved Riverpod provider (not async). Use a dedicated ShellRoute navigator key per tab branch to anchor state independently of role-driven rebuilds. Write integration tests for the full navigation graph.

Contingency: If StatefulShellRoute state loss is confirmed during QA, fall back to a manual tab state preservation approach using a TabStateManager service that caches the last route per tab and restores it after role switches, decoupling tab state from the shell lifecycle.

medium impact high prob scope

The role-based home screen must render three significantly different layouts (coordinator dashboard, peer mentor activity summary, org admin overview). If these variants are implemented as a single widget with conditionals, the file will become unmaintainable and difficult to test in isolation, especially as each variant grows with downstream feature additions.

Mitigation & Contingency

Mitigation: Design the role-based home screen as a router/dispatcher widget that delegates to three separate variant widgets (CoordinatorHomeView, PeerMentorHomeView, OrgAdminHomeView). Each variant is independently testable and can be developed by separate team members in parallel.

Contingency: If variant coupling has already occurred before this risk is addressed, refactor to the dispatcher pattern in a dedicated cleanup task before feature handoff. The dispatcher pattern is a straightforward extraction that carries low refactoring risk.

medium impact medium prob integration

The no-access screen must link global admin users to the correct admin portal URL, which may differ per organization (NHF, HLF, Blindeforbundet each have their own admin portals). Hardcoding a single URL will result in wrong or broken links for some global admin users.

Mitigation & Contingency

Mitigation: Source the admin portal URL from the organization's configuration record in Supabase rather than hardcoding it. The no-access screen reads the active org context and resolves the portal URL dynamically. Provide a safe fallback to a generic Norse Digital Products support page if the URL is not configured.

Contingency: If dynamic URL resolution is not ready when the no-access screen ships, display a static instruction to contact the organization's administrator along with a support email address as an interim measure, and track the URL configuration task as a follow-up.