52 lines
1.4 KiB
TypeScript
52 lines
1.4 KiB
TypeScript
|
|
import { useEffect } from 'react';
|
||
|
|
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||
|
|
import { useAuth } from '@/hooks/useAuth';
|
||
|
|
import { useToast } from '@/hooks/useToast';
|
||
|
|
import type { Role } from '@/api/types';
|
||
|
|
import { LoadingState } from './LoadingState';
|
||
|
|
|
||
|
|
interface ProtectedRouteProps {
|
||
|
|
/** Allowed roles. If omitted, any authenticated user passes. */
|
||
|
|
roles?: Role[];
|
||
|
|
/** Where to send users who lack the required role. */
|
||
|
|
redirectOnRoleDenied?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gate component: handles auth + role checks before rendering nested routes.
|
||
|
|
* - No token / no user → /login
|
||
|
|
* - Wrong role → redirect + "Accès refusé" toast (AC-3.7)
|
||
|
|
*/
|
||
|
|
export function ProtectedRoute({
|
||
|
|
roles,
|
||
|
|
redirectOnRoleDenied = '/engagements',
|
||
|
|
}: ProtectedRouteProps): JSX.Element {
|
||
|
|
const { user, status } = useAuth();
|
||
|
|
const { push } = useToast();
|
||
|
|
const location = useLocation();
|
||
|
|
|
||
|
|
const roleDenied = Boolean(
|
||
|
|
status === 'authenticated' && user && roles && !roles.includes(user.role),
|
||
|
|
);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (roleDenied) {
|
||
|
|
push('Accès refusé', 'error');
|
||
|
|
}
|
||
|
|
}, [roleDenied, push]);
|
||
|
|
|
||
|
|
if (status === 'loading') {
|
||
|
|
return <LoadingState label="Loading session…" />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (status === 'unauthenticated' || !user) {
|
||
|
|
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (roleDenied) {
|
||
|
|
return <Navigate to={redirectOnRoleDenied} replace />;
|
||
|
|
}
|
||
|
|
|
||
|
|
return <Outlet />;
|
||
|
|
}
|