/** * 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, useMemo } 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 * * * * ``` */ export function RouteGuard({ children, config = {}, loadingComponent, unauthorizedComponent, }: RouteGuardProps) { const router = useRouter(); const authContext = useAuth(); const [gateway] = useState(() => new AuthGateway(authContext, config)); const [isChecking, setIsChecking] = useState(true); // Calculate access state const accessState = useMemo(() => { gateway.refresh(); return { canAccess: gateway.canAccess(), reason: gateway.getBlockMessage(), redirectPath: gateway.getUnauthorizedRedirectPath(), }; }, [authContext.session, authContext.loading, gateway]); // Handle the loading state and redirects useEffect(() => { // If we're loading, stay in checking state if (authContext.loading) { setIsChecking(true); return; } // Done loading, can exit checking state setIsChecking(false); // If we can't access and should redirect, do it if (!accessState.canAccess && config.redirectOnUnauthorized !== false) { const timer = setTimeout(() => { router.push(accessState.redirectPath); }, 500); return () => clearTimeout(timer); } }, [authContext.loading, accessState.canAccess, accessState.redirectPath, config.redirectOnUnauthorized, router]); // Show loading state if (isChecking || authContext.loading) { return loadingComponent || (
); } // Show unauthorized state (only if not redirecting) if (!accessState.canAccess && config.redirectOnUnauthorized === false) { return unauthorizedComponent || (

Access Denied

{accessState.reason}

); } // Show redirecting state if (!accessState.canAccess && config.redirectOnUnauthorized !== false) { // Don't show a message, just redirect silently // The redirect happens in the useEffect above return null; } // 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(), }; }