Files
bakery-ia/frontend/src/router/ProtectedRoute.tsx
2025-08-28 10:41:04 +02:00

307 lines
8.9 KiB
TypeScript

/**
* Protected Route component for handling authentication and authorization
*/
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthUser, useIsAuthenticated, useAuthLoading } from '../stores';
import { RouteConfig, canAccessRoute, ROUTES } from './routes.config';
interface ProtectedRouteProps {
children: React.ReactNode;
route?: RouteConfig;
fallback?: React.ReactNode;
redirectTo?: string;
}
interface LoadingSpinnerProps {
message?: string;
}
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ message = 'Cargando...' }) => (
<div className="flex items-center justify-center min-h-screen bg-bg-primary">
<div className="flex flex-col items-center gap-4">
<div className="spinner spinner-lg"></div>
<p className="text-text-secondary text-sm">{message}</p>
</div>
</div>
);
const UnauthorizedPage: React.FC = () => (
<div className="flex items-center justify-center min-h-screen bg-bg-primary">
<div className="text-center max-w-md mx-auto px-6">
<div className="mb-6">
<div className="w-16 h-16 mx-auto mb-4 bg-color-error rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-text-inverse"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.732 19.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h1 className="text-2xl font-bold text-text-primary mb-2">
Acceso no autorizado
</h1>
<p className="text-text-secondary mb-6">
No tienes permisos para acceder a esta página. Contacta con tu administrador si crees que esto es un error.
</p>
</div>
<div className="flex flex-col gap-3">
<button
onClick={() => window.history.back()}
className="btn btn-primary"
>
Volver atrás
</button>
<button
onClick={() => window.location.href = ROUTES.DASHBOARD}
className="btn btn-outline"
>
Ir al Panel de Control
</button>
</div>
</div>
</div>
);
const ForbiddenPage: React.FC = () => (
<div className="flex items-center justify-center min-h-screen bg-bg-primary">
<div className="text-center max-w-md mx-auto px-6">
<div className="mb-6">
<div className="w-16 h-16 mx-auto mb-4 bg-color-warning rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-text-inverse"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L5.636 5.636"
/>
</svg>
</div>
<h1 className="text-2xl font-bold text-text-primary mb-2">
Acceso restringido
</h1>
<p className="text-text-secondary mb-6">
Tu cuenta no tiene los permisos necesarios para ver esta sección. Contacta con tu administrador para solicitar acceso.
</p>
</div>
<div className="flex flex-col gap-3">
<button
onClick={() => window.history.back()}
className="btn btn-primary"
>
Volver atrás
</button>
<button
onClick={() => window.location.href = ROUTES.DASHBOARD}
className="btn btn-outline"
>
Ir al Panel de Control
</button>
</div>
</div>
</div>
);
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
route,
fallback,
redirectTo,
}) => {
const user = useAuthUser();
const isAuthenticated = useIsAuthenticated();
const isLoading = useAuthLoading();
const location = useLocation();
// Show loading spinner while checking authentication
if (isLoading) {
return fallback || <LoadingSpinner message="Verificando autenticación..." />;
}
// If route doesn't require auth, render children
if (route && !route.requiresAuth) {
return <>{children}</>;
}
// If not authenticated and route requires auth, redirect to login
if (!isAuthenticated) {
const redirectPath = redirectTo || ROUTES.LOGIN;
const returnUrl = location.pathname + location.search;
return (
<Navigate
to={`${redirectPath}?returnUrl=${encodeURIComponent(returnUrl)}`}
replace
/>
);
}
// If no route config is provided, just check authentication
if (!route) {
return <>{children}</>;
}
// Get user roles and permissions
const userRoles = user?.role ? [user.role] : [];
const userPermissions: string[] = [];
// Check if user can access this route
const canAccess = canAccessRoute(route, isAuthenticated, userRoles, userPermissions);
if (!canAccess) {
// Check if it's a permission issue or role issue
const hasRequiredRoles = !route.requiredRoles ||
route.requiredRoles.some(role => userRoles.includes(role));
if (!hasRequiredRoles) {
return <UnauthorizedPage />;
} else {
return <ForbiddenPage />;
}
}
// User has access, render the protected content
return <>{children}</>;
};
// Higher-order component for route protection
export const withProtectedRoute = <P extends object>(
Component: React.ComponentType<P>,
route: RouteConfig,
options?: {
fallback?: React.ReactNode;
redirectTo?: string;
}
) => {
const ProtectedComponent = (props: P) => (
<ProtectedRoute
route={route}
fallback={options?.fallback}
redirectTo={options?.redirectTo}
>
<Component {...props} />
</ProtectedRoute>
);
ProtectedComponent.displayName = `withProtectedRoute(${Component.displayName || Component.name})`;
return ProtectedComponent;
};
// Hook for checking route access
export const useRouteAccess = (route: RouteConfig) => {
const user = useAuthUser();
const isAuthenticated = useIsAuthenticated();
const isLoading = useAuthLoading();
const userRoles = user?.role ? [user.role] : [];
const userPermissions: string[] = [];
const canAccess = canAccessRoute(route, isAuthenticated, userRoles, userPermissions);
return {
canAccess,
isLoading,
isAuthenticated,
userRoles,
userPermissions,
hasRequiredRoles: !route.requiredRoles ||
route.requiredRoles.some(role => userRoles.includes(role)),
hasRequiredPermissions: !route.requiredPermissions ||
route.requiredPermissions.every(permission => userPermissions.includes(permission)),
};
};
// Component for conditionally rendering content based on permissions
interface ConditionalRenderProps {
children: React.ReactNode;
requiredPermissions?: string[];
requiredRoles?: string[];
requireAll?: boolean; // If true, requires ALL permissions/roles, otherwise ANY
fallback?: React.ReactNode;
}
export const ConditionalRender: React.FC<ConditionalRenderProps> = ({
children,
requiredPermissions = [],
requiredRoles = [],
requireAll = true,
fallback = null,
}) => {
const user = useAuthUser();
const isAuthenticated = useIsAuthenticated();
if (!isAuthenticated || !user) {
return <>{fallback}</>;
}
const userRoles = user.role ? [user.role] : [];
const userPermissions: string[] = [];
// Check roles
let hasRoles = true;
if (requiredRoles.length > 0) {
if (requireAll) {
hasRoles = requiredRoles.every(role => userRoles.includes(role));
} else {
hasRoles = requiredRoles.some(role => userRoles.includes(role));
}
}
// Check permissions
let hasPermissions = true;
if (requiredPermissions.length > 0) {
if (requireAll) {
hasPermissions = requiredPermissions.every(permission => userPermissions.includes(permission));
} else {
hasPermissions = requiredPermissions.some(permission => userPermissions.includes(permission));
}
}
if (hasRoles && hasPermissions) {
return <>{children}</>;
}
return <>{fallback}</>;
};
// Route guard for admin-only routes
export const AdminRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<ConditionalRender
requiredRoles={['admin', 'super_admin']}
requireAll={false}
fallback={<UnauthorizedPage />}
>
{children}
</ConditionalRender>
);
};
// Route guard for manager-level routes
export const ManagerRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<ConditionalRender
requiredRoles={['admin', 'super_admin', 'manager']}
requireAll={false}
fallback={<UnauthorizedPage />}
>
{children}
</ConditionalRender>
);
};