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>
119 lines
3.4 KiB
TypeScript
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;
|