ADD new frontend
This commit is contained in:
307
frontend/src/router/ProtectedRoute.tsx
Normal file
307
frontend/src/router/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user