go_router_guards 1.0.0+1
go_router_guards: ^1.0.0+1 copied to clipboard
A flexible and extensible guard system for Go Router that allows you to chain multiple guards together for route protection.
go_router_guards #
A flexible and extensible guard system for Go Router that enables type-safe route protection with complex boolean logic support.
Quick Start #
Installation #
Add go_router_guards to your pubspec.yaml:
dependencies:
go_router_guards: ^1.0.0+1
Type-Safe Routes with Guard Expressions #
Following VGV's routing best practices, use type-safe routes with guard expressions:
import 'package:go_router_guards/go_router_guards.dart';
// Define type-safe routes
@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData {
const LoginRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const LoginScreen();
}
}
@TypedGoRoute<ProtectedRoute>(path: '/protected')
class ProtectedRoute extends GoRouteData with GuardedRoute {
const ProtectedRoute();
@override
GuardExpression get guards => Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
]);
@override
Widget build(BuildContext context, GoRouterState state) {
return const ProtectedScreen();
}
}
// Create type-safe guards
class AuthenticationGuard implements RouteGuard {
@override
FutureOr<String?> redirect(BuildContext context, GoRouterState state) async {
final authState = context.read<AuthCubit>().state;
if (!authState.isAuthenticated) {
return LoginRoute().location; // Type-safe navigation
}
return null;
}
}
// Navigate using type-safe routes
ElevatedButton(
onPressed: () => ProtectedRoute().go(context),
child: const Text('Go to Protected Route'),
)
Core Features #
RouteGuard Interface #
Implement guards to protect your routes:
mixin RouteGuard {
FutureOr<String?> redirect(BuildContext context, GoRouterState state);
}
- Return
nullto allow access - Return a route location (e.g.,
LoginRoute().location) to redirect
Guard Expressions with Logical Operators #
Create complex guard logic using boolean expressions:
// Simple AND: both must pass
Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
])
// Simple OR: either can pass
Guards.anyOf([
Guards.guard(AuthenticationGuard()),
Guards.guard(AdminGuard()),
])
// Complex expression: (a & b) || c
Guards.anyOf([
Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
]),
Guards.guard(SuperAdminGuard()),
])
// Multiple guards with ALL: all must pass
Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
Guards.guard(SubscriptionGuard()),
Guards.guard(PaymentGuard()),
])
// Multiple guards with ANY OF: any can pass
Guards.anyOf([
Guards.guard(AuthenticationGuard()),
Guards.guard(AdminGuard()),
Guards.guard(SuperAdminGuard()),
])
// Multiple guards with ONE OF: exactly one must pass
Guards.oneOf([
Guards.guard(AuthenticationGuard()),
Guards.guard(AdminGuard()),
Guards.guard(SuperAdminGuard()),
], '/unauthorized')
// ONE OF: exactly one must pass
Guards.oneOf([
Guards.guard(AuthenticationGuard()),
Guards.guard(AdminGuard()),
], '/unauthorized')
GuardedRoute Mixin #
Add guard functionality to your route classes:
class AdminRoute extends GoRouteData with GuardedRoute {
const AdminRoute();
@override
GuardExpression get guards => Guards.anyOf([
Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
]),
Guards.guard(SuperAdminGuard()),
]);
@override
Widget build(BuildContext context, GoRouterState state) {
return const AdminScreen();
}
}
Complex Guard Logic #
// Route accessible by:
// - Authenticated users with admin role AND premium subscription
// - OR super admins
// - OR users with special access token
class PremiumAdminRoute extends GoRouteData with GuardedRoute {
const PremiumAdminRoute();
@override
GuardExpression get guards => Guards.anyOf([
Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
Guards.guard(SubscriptionGuard()),
]),
Guards.anyOf([
Guards.guard(SuperAdminGuard()),
Guards.guard(SpecialAccessGuard()),
]),
]);
@override
Widget build(BuildContext context, GoRouterState state) {
return const PremiumAdminScreen();
}
}
Conditional Guards #
class ConditionalGuard implements RouteGuard {
@override
FutureOr<String?> redirect(BuildContext context, GoRouterState state) async {
final appState = context.read<AppCubit>().state;
if (appState.isMaintenanceMode) {
return MaintenanceRoute().location;
}
if (appState.isOffline) {
return OfflineRoute().location;
}
return null;
}
}
Testing Guards #
test('complex guard expression', () async {
final expression = Guards.anyOf([
Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
]),
Guards.guard(SuperAdminGuard()),
]);
// Test with authenticated admin
when(mockAuthCubit.state).thenReturn(AuthenticatedState());
when(mockUserCubit.state).thenReturn(UserState(roles: ['admin']));
final result = await expression.execute(mockContext, mockState);
expect(result, isNull); // Access granted
});
Best Practices #
1. Use Type-Safe Navigation #
Always use type-safe routes for navigation:
// ✅ Good - Type-safe
context.go(ProtectedRoute().location);
ProtectedRoute().go(context);
// ❌ Bad - Hardcoded paths
context.go('/protected');
2. Order Guards by Performance #
Order guards from fastest to slowest in ALL expressions:
Guards.all([
Guards.guard(AppInitializationGuard()), // Fast check
Guards.guard(AuthenticationGuard()), // Medium check
Guards.guard(AsyncGuard()), // Slow async check
])
3. Create Reusable Guard Expressions #
Extract common guard logic:
class PremiumFeatureGuard implements RouteGuard {
@override
FutureOr<String?> redirect(BuildContext context, GoRouterState state) async {
final userState = context.read<UserCubit>().state;
if (!userState.hasPremiumAccess) {
return UpgradeRoute().location;
}
return null;
}
}
// Reusable expression
final premiumGuard = Guards.guard(PremiumFeatureGuard());
final adminGuard = Guards.guard(RoleGuard(['admin']));
// Use in multiple routes
final adminPremiumGuard = Guards.all([adminGuard, premiumGuard]);
4. Handle Guard Failures Gracefully #
class RobustGuard implements RouteGuard {
@override
FutureOr<String?> redirect(BuildContext context, GoRouterState state) async {
try {
final userState = context.read<UserCubit>().state;
if (!userState.isAuthenticated) {
return LoginRoute().location;
}
return null;
} catch (e) {
return ErrorRoute().location;
}
}
}
Testing #
Unit Testing Guard Expressions #
test('AND expression with both guards passing', () async {
final expression = Guards.all([
Guards.guard(AuthenticationGuard()),
Guards.guard(RoleGuard(['admin'])),
]);
when(mockAuthCubit.state).thenReturn(AuthenticatedState());
when(mockUserCubit.state).thenReturn(UserState(roles: ['admin']));
final result = await expression.execute(mockContext, mockState);
expect(result, isNull);
});
Integration Testing #
testWidgets('complex guard expression redirects correctly', (tester) async {
await tester.pumpWidget(MyApp());
await tester.tap(find.text('Premium Admin Route'));
await tester.pumpAndSettle();
// Should redirect to login if not authenticated
expect(find.text('Login'), findsOneWidget);
});
Contributing #
See CONTRIBUTING.md for details.
License #
This project is licensed under the MIT License - see the LICENSE file for details.