mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: add RBAC permission system to admin cabinet frontend
- Permission store (Zustand) with wildcard matching and role level checks - PermissionRoute guard for route-level access control - PermissionGate component for element-level visibility - Admin Roles page: CRUD, permission editor, role assignment - Admin Policies page: ABAC policy management (time/IP conditions) - Admin Audit Log page: filterable log viewer with CSV export - AdminPanel sidebar navigation gated by permissions - 4 locale files updated (en, ru, zh, fa) with RBAC translations - API client module for all RBAC endpoints
This commit is contained in:
42
src/components/auth/PermissionGate.tsx
Normal file
42
src/components/auth/PermissionGate.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { usePermissionStore } from '../../store/permissions';
|
||||
|
||||
interface PermissionGateProps {
|
||||
children: React.ReactNode;
|
||||
/** Single permission to check */
|
||||
permission?: string;
|
||||
/** Multiple permissions (OR by default, AND if requireAll is true) */
|
||||
permissions?: string[];
|
||||
/** When true, ALL listed permissions are required instead of ANY */
|
||||
requireAll?: boolean;
|
||||
/** Content to render when the user lacks the required permissions */
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PermissionGate({
|
||||
children,
|
||||
permission,
|
||||
permissions,
|
||||
requireAll = false,
|
||||
fallback = null,
|
||||
}: PermissionGateProps) {
|
||||
const hasPermission = usePermissionStore((state) => state.hasPermission);
|
||||
const hasAnyPermission = usePermissionStore((state) => state.hasAnyPermission);
|
||||
const hasAllPermissions = usePermissionStore((state) => state.hasAllPermissions);
|
||||
|
||||
let hasAccess = false;
|
||||
|
||||
if (permission) {
|
||||
hasAccess = hasPermission(permission);
|
||||
} else if (permissions && permissions.length > 0) {
|
||||
hasAccess = requireAll ? hasAllPermissions(...permissions) : hasAnyPermission(...permissions);
|
||||
} else {
|
||||
// No permissions specified — allow access
|
||||
hasAccess = true;
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
66
src/components/auth/PermissionRoute.tsx
Normal file
66
src/components/auth/PermissionRoute.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Navigate, useLocation } from 'react-router';
|
||||
import { useAuthStore } from '../../store/auth';
|
||||
import { usePermissionStore } from '../../store/permissions';
|
||||
import { saveReturnUrl } from '../../utils/token';
|
||||
import PageLoader from '../common/PageLoader';
|
||||
import Layout from '../layout/Layout';
|
||||
|
||||
interface PermissionRouteProps {
|
||||
children: React.ReactNode;
|
||||
/** Single permission to check */
|
||||
permission?: string;
|
||||
/** Multiple permissions (OR by default, AND if requireAll is true) */
|
||||
permissions?: string[];
|
||||
/** When true, ALL listed permissions are required instead of ANY */
|
||||
requireAll?: boolean;
|
||||
}
|
||||
|
||||
export function PermissionRoute({
|
||||
children,
|
||||
permission,
|
||||
permissions,
|
||||
requireAll = false,
|
||||
}: PermissionRouteProps) {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const isLoading = useAuthStore((state) => state.isLoading);
|
||||
const isAdmin = useAuthStore((state) => state.isAdmin);
|
||||
const location = useLocation();
|
||||
|
||||
const permissionsLoaded = usePermissionStore((state) => state.isLoaded);
|
||||
const hasPermission = usePermissionStore((state) => state.hasPermission);
|
||||
const hasAnyPermission = usePermissionStore((state) => state.hasAnyPermission);
|
||||
const hasAllPermissions = usePermissionStore((state) => state.hasAllPermissions);
|
||||
|
||||
// Still loading auth state, or admin but permissions not fetched yet
|
||||
if (isLoading || (isAdmin && !permissionsLoaded)) {
|
||||
return <PageLoader variant="light" />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
saveReturnUrl();
|
||||
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
// Check permissions if specified
|
||||
const needsPermissionCheck = permission || (permissions && permissions.length > 0);
|
||||
|
||||
if (needsPermissionCheck) {
|
||||
let hasAccess = false;
|
||||
|
||||
if (permission) {
|
||||
hasAccess = hasPermission(permission);
|
||||
} else if (permissions && permissions.length > 0) {
|
||||
hasAccess = requireAll ? hasAllPermissions(...permissions) : hasAnyPermission(...permissions);
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return <Navigate to="/admin" replace />;
|
||||
}
|
||||
}
|
||||
|
||||
return <Layout>{children}</Layout>;
|
||||
}
|
||||
Reference in New Issue
Block a user