erp-construccion-frontend-v2/web/src/components/auth/ProtectedRoute.tsx
Adrian Flores Cortes a03bed842f [REMEDIATION] feat: Frontend remediation - auth, finance, contracts, session management
Add auth components, finance pages/hooks/services, contract components.
Enhance LoginPage, AdminLayout, hooks. Remove legacy apiClient.
Add mock data services for development. Addresses frontend gaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:18:22 -06:00

119 lines
3.4 KiB
TypeScript

/**
* ProtectedRoute Component
* Guards routes that require authentication with role-based access control
* Based on gamilit implementation
*/
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../stores/authStore';
import { LoadingSpinner } from '../common/LoadingSpinner';
interface ProtectedRouteProps {
children: React.ReactNode;
/** Roles allowed to access this route */
allowedRoles?: string[];
/** Custom redirect path for unauthenticated users */
redirectTo?: string;
}
export function ProtectedRoute({
children,
allowedRoles,
redirectTo = '/auth/login',
}: ProtectedRouteProps) {
const user = useAuthStore((state) => state.user);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const isLoading = useAuthStore((state) => state.isLoading);
const isInitialized = useAuthStore((state) => state.isInitialized);
const checkSession = useAuthStore((state) => state.checkSession);
const location = useLocation();
// Show loading while checking auth state
if (isLoading || !isInitialized) {
return (
<div className="min-h-screen flex items-center justify-center bg-background dark:bg-background">
<div className="text-center">
<LoadingSpinner size="lg" />
<p className="mt-4 text-foreground-muted dark:text-foreground-muted">
Verificando sesión...
</p>
</div>
</div>
);
}
// Check if session is still valid
const sessionValid = checkSession();
// Not authenticated - redirect to login
if (!isAuthenticated || !sessionValid) {
return (
<Navigate
to={redirectTo}
state={{ from: location }}
replace
/>
);
}
// Check role-based access if roles are specified
if (allowedRoles && allowedRoles.length > 0) {
const userRole = user?.role || '';
const hasRequiredRole = allowedRoles.includes(userRole);
if (!hasRequiredRole) {
// User doesn't have required role - redirect to unauthorized page
return (
<Navigate
to="/unauthorized"
state={{ from: location, requiredRoles: allowedRoles }}
replace
/>
);
}
}
// User is authenticated and has required role
return <>{children}</>;
}
/**
* PublicRoute Component
* Redirects authenticated users away from public pages (login, register)
*/
interface PublicRouteProps {
children: React.ReactNode;
/** Where to redirect authenticated users */
redirectTo?: string;
}
export function PublicRoute({
children,
redirectTo = '/admin/dashboard',
}: PublicRouteProps) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const isLoading = useAuthStore((state) => state.isLoading);
const isInitialized = useAuthStore((state) => state.isInitialized);
const location = useLocation();
// Show loading while checking auth state
if (isLoading || !isInitialized) {
return (
<div className="min-h-screen flex items-center justify-center bg-background dark:bg-background">
<LoadingSpinner size="lg" />
</div>
);
}
// If user is already authenticated, redirect to dashboard
if (isAuthenticated) {
// Check if there's a "from" location to redirect back to
const from = (location.state as { from?: Location })?.from?.pathname || redirectTo;
return <Navigate to={from} replace />;
}
return <>{children}</>;
}
export default ProtectedRoute;