Files
gridpilot.gg/apps/website/lib/gateways/RouteGuard.tsx
2026-01-01 12:10:35 +01:00

137 lines
4.0 KiB
TypeScript

/**
* Component: RouteGuard
*
* Higher-order component that protects routes using Gateways and Blockers.
* Follows clean architecture by separating concerns:
* - Gateway handles access logic
* - Blocker handles prevention logic
* - Component handles UI rendering
*/
'use client';
import { ReactNode, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext';
import { AuthGateway, AuthGatewayConfig } from './AuthGateway';
import { LoadingState } from '@/components/shared/LoadingState';
interface RouteGuardProps {
children: ReactNode;
config?: AuthGatewayConfig;
/**
* Custom loading component (optional)
*/
loadingComponent?: ReactNode;
/**
* Custom unauthorized component (optional)
*/
unauthorizedComponent?: ReactNode;
}
/**
* RouteGuard Component
*
* Protects child components based on authentication and authorization rules.
* Uses Gateway pattern for access control.
*
* Usage:
* ```tsx
* <RouteGuard config={{ requiredRoles: ['owner', 'admin'] }}>
* <AdminDashboard />
* </RouteGuard>
* ```
*/
export function RouteGuard({
children,
config = {},
loadingComponent,
unauthorizedComponent,
}: RouteGuardProps) {
const router = useRouter();
const authContext = useAuth();
const [gateway] = useState(() => new AuthGateway(authContext, config));
const [accessState, setAccessState] = useState(gateway.getAccessState());
// Update gateway when auth context changes
useEffect(() => {
gateway.refresh();
setAccessState(gateway.getAccessState());
}, [authContext.session, authContext.loading, gateway]);
// Handle redirects
useEffect(() => {
if (!accessState.canAccess && !accessState.isLoading) {
if (config.redirectOnUnauthorized !== false) {
const redirectPath = gateway.getUnauthorizedRedirectPath();
// Use a small delay to show unauthorized message briefly
const timer = setTimeout(() => {
router.push(redirectPath);
}, 500);
return () => clearTimeout(timer);
}
}
}, [accessState, gateway, router, config.redirectOnUnauthorized]);
// Show loading state
if (accessState.isLoading) {
return loadingComponent || (
<div className="flex items-center justify-center min-h-screen">
<LoadingState message="Loading..." className="min-h-screen" />
</div>
);
}
// Show unauthorized state
if (!accessState.canAccess) {
return unauthorizedComponent || (
<div className="flex items-center justify-center min-h-screen">
<div className="bg-iron-gray p-8 rounded-lg border border-charcoal-outline max-w-md text-center">
<h2 className="text-xl font-bold text-racing-red mb-4">Access Denied</h2>
<p className="text-gray-300 mb-6">{accessState.reason}</p>
<button
onClick={() => router.push('/auth/login')}
className="px-4 py-2 bg-primary-blue text-white rounded hover:bg-blue-600 transition-colors"
>
Go to Login
</button>
</div>
</div>
);
}
// Render protected content
return <>{children}</>;
}
/**
* useRouteGuard Hook
*
* Hook for programmatic access control within components.
*
* Usage:
* ```tsx
* const { canAccess, reason, isLoading } = useRouteGuard({ requiredRoles: ['admin'] });
* ```
*/
export function useRouteGuard(config: AuthGatewayConfig = {}) {
const authContext = useAuth();
const [gateway] = useState(() => new AuthGateway(authContext, config));
const [state, setState] = useState(gateway.getAccessState());
useEffect(() => {
gateway.refresh();
setState(gateway.getAccessState());
}, [authContext.session, authContext.loading, gateway]);
return {
canAccess: state.canAccess,
reason: state.reason,
isLoading: state.isLoading,
isAuthenticated: state.isAuthenticated,
enforceAccess: () => gateway.enforceAccess(),
redirectIfUnauthorized: () => gateway.redirectIfUnauthorized(),
};
}