Files
mimic/frontend/src/components/ProtectedRoute.tsx
2026-06-21 23:23:36 +02:00

54 lines
1.5 KiB
TypeScript

import { useEffect } from 'react';
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
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 { t } = useTranslation();
const location = useLocation();
const roleDenied = Boolean(
status === 'authenticated' && user && roles && !roles.includes(user.role),
);
useEffect(() => {
if (roleDenied) {
push(t('auth.forbidden'), 'error');
}
}, [roleDenied, push, t]);
if (status === 'loading') {
return <LoadingState label={t('auth.loadingSession')} />;
}
if (status === 'unauthenticated' || !user) {
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
if (roleDenied) {
return <Navigate to={redirectOnRoleDenied} replace />;
}
return <Outlet />;
}