route_guards Package
import { Card, CardGrid } from ‘@astrojs/starlight/components’;
route_guards
Section titled “route_guards”The route_guards
package provides the core, framework-agnostic foundation for creating navigation guards. It defines the basic abstractions and utilities that can work with any routing system.
Overview
Section titled “Overview”This package contains the fundamental building blocks for creating route protection systems:
- RouteGuard: Base class for creating custom guards
- NavigationResolver: Controls navigation flow with middleware pattern
- Guards: Utility class for combining and composing guards
- GuardResult: Represents the outcome of guard execution
Key Features
Section titled “Key Features”Installation
Section titled “Installation”dart pub add route_guards
Core Classes
Section titled “Core Classes”RouteGuard
Section titled “RouteGuard”The base class for all guards. Extend this to create custom protection logic:
abstract class RouteGuard { const RouteGuard();
/// Override this method to implement guard logic FutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state, );}
Example Implementation:
class AuthGuard extends RouteGuard { @override FutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state, ) async { final isAuthenticated = await checkAuth(); if (isAuthenticated) { resolver.next(); // Allow navigation } else { resolver.redirect('/login'); // Redirect to login } }}
NavigationResolver
Section titled “NavigationResolver”Controls navigation flow within guards. Provides three actions:
class NavigationResolver { /// Allow navigation to continue void next();
/// Redirect to a different path void redirect(String path);
/// Block navigation entirely void block();}
Usage Pattern:
@overrideFutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state,) async { if (await isAllowed()) { resolver.next(); } else if (shouldRedirect()) { resolver.redirect('/alternative-path'); } else { resolver.block(); }}
Guards Utility
Section titled “Guards Utility”Combines multiple guards with logical operations:
class Guards { /// All guards must pass static RouteGuard all(List<RouteGuard> guards);
/// At least one guard must pass static RouteGuard anyOf(List<RouteGuard> guards);
/// Exactly one guard must pass static RouteGuard oneOf(List<RouteGuard> guards);}
Examples:
// All guards must pass (AND logic)final adminGuard = Guards.all([ AuthGuard(), RoleGuard(['admin']), PermissionGuard(['read_users']),]);
// Any guard can pass (OR logic)final memberGuard = Guards.anyOf([ SubscriptionGuard(), TrialGuard(), AdminGuard(),]);
// Exactly one guard must pass (XOR logic)final exclusiveGuard = Guards.oneOf([ DevModeGuard(), ProductionAccessGuard(),]);
GuardResult
Section titled “GuardResult”Represents the outcome of guard execution:
class GuardResult { final bool continueNavigation; final String? redirectPath;
// Factory constructors GuardResult.next(); // Allow navigation GuardResult.redirect(String path); // Redirect GuardResult.block(); // Block navigation}
Advanced Usage
Section titled “Advanced Usage”Custom Guard Combinations
Section titled “Custom Guard Combinations”Create custom guard combinations for complex scenarios:
class ConditionalGuard extends RouteGuard { const ConditionalGuard({ required this.guard, this.includedPaths = const [], this.excludedPaths = const [], });
final RouteGuard guard; final List<String> includedPaths; final List<String> excludedPaths;
@override FutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state, ) async { final path = _extractPath(state);
// Check if path should be excluded if (excludedPaths.any((excluded) => path.startsWith(excluded))) { resolver.next(); return; }
// Check if path should be included if (includedPaths.isNotEmpty && !includedPaths.any((included) => path.startsWith(included))) { resolver.next(); return; }
// Apply the guard await guard.onNavigation(resolver, context, state); }}
Stateful Guards
Section titled “Stateful Guards”Create guards that maintain state across navigation events:
class RateLimitGuard extends RouteGuard { RateLimitGuard({required this.maxAttempts, required this.timeWindow});
final int maxAttempts; final Duration timeWindow; final Map<String, List<DateTime>> _attempts = {};
@override FutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state, ) { final userId = _extractUserId(context); final now = DateTime.now();
// Clean old attempts _attempts[userId]?.removeWhere( (attempt) => now.difference(attempt) > timeWindow, );
final attempts = _attempts[userId] ?? [];
if (attempts.length >= maxAttempts) { resolver.redirect('/rate-limited'); } else { attempts.add(now); _attempts[userId] = attempts; resolver.next(); } }}
Dependency Injection Support
Section titled “Dependency Injection Support”Integrate with dependency injection systems:
class ServiceGuard extends RouteGuard { const ServiceGuard({ required this.authService, required this.userService, });
final AuthService authService; final UserService userService;
@override FutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state, ) async { final user = await authService.getCurrentUser(); if (user == null) { resolver.redirect('/login'); return; }
final permissions = await userService.getPermissions(user.id); if (permissions.contains('access_feature')) { resolver.next(); } else { resolver.block(); } }}
Testing Guards
Section titled “Testing Guards”The package provides excellent testing support:
void main() { group('AuthGuard', () { late AuthGuard guard; late MockAuthService authService;
setUp(() { authService = MockAuthService(); guard = AuthGuard(authService: authService); });
test('allows navigation when authenticated', () async { when(() => authService.isAuthenticated()).thenAnswer((_) async => true);
final result = await guard.executeWithResolver( MockContext(), MockState(), );
expect(result.continueNavigation, isTrue); });
test('redirects when not authenticated', () async { when(() => authService.isAuthenticated()).thenAnswer((_) async => false);
final result = await guard.executeWithResolver( MockContext(), MockState(), );
expect(result.continueNavigation, isFalse); expect(result.redirectPath, '/login'); }); });}
Error Handling
Section titled “Error Handling”Handle errors gracefully in your guards:
class SafeAuthGuard extends RouteGuard { @override FutureOr<void> onNavigation( NavigationResolver resolver, Object context, Object state, ) async { try { final isAuthenticated = await checkAuth(); if (isAuthenticated) { resolver.next(); } else { resolver.redirect('/login'); } } catch (error) { // Log error and allow navigation to prevent app breakage print('Auth check failed: $error'); resolver.next(); } }}
Best Practices
Section titled “Best Practices”- Keep Guards Simple: Each guard should have a single responsibility
- Handle Errors: Always handle potential exceptions
- Use Composition: Combine simple guards instead of creating complex ones
- Test Thoroughly: Write tests for all guard logic and edge cases
- Performance: Avoid expensive operations in guards when possible
- Logging: Add logging for debugging and monitoring
Framework Integrations
Section titled “Framework Integrations”While this package is framework-agnostic, it’s designed to work with:
- go_router_guards: Official Go Router integration
- Custom integrations: Build your own router integration
API Reference
Section titled “API Reference”Classes
Section titled “Classes”Class | Description |
---|---|
RouteGuard | Base class for all guards |
NavigationResolver | Controls navigation flow |
GuardResult | Result of guard execution |
Guards | Utility for combining guards |
Methods
Section titled “Methods”Method | Returns | Description |
---|---|---|
Guards.all(guards) | RouteGuard | All guards must pass |
Guards.anyOf(guards) | RouteGuard | Any guard can pass |
Guards.oneOf(guards) | RouteGuard | Exactly one guard must pass |
resolver.next() | void | Allow navigation |
resolver.redirect(path) | void | Redirect to path |
resolver.block() | void | Block navigation |
Migration Guide
Section titled “Migration Guide”From v0.x to v1.x
Section titled “From v0.x to v1.x”onNavigation
now takesNavigationResolver
as first parameterGuardResult
is now created through factory constructors- Async support is now built-in
Contributing
Section titled “Contributing”The route_guards
package welcomes contributions! See the contribution guide for details.