/**
* 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(),
};
}