153 lines
4.5 KiB
TypeScript
153 lines
4.5 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, 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
|
|
* <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 [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 || (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<LoadingState message="Verifying authentication..." className="min-h-screen" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Show unauthorized state (only if not redirecting)
|
|
if (!accessState.canAccess && config.redirectOnUnauthorized === false) {
|
|
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>
|
|
);
|
|
}
|
|
|
|
// 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(),
|
|
};
|
|
} |