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:
Fringg
2026-02-25 03:02:52 +03:00
parent 48be067d1b
commit 874ee2682e
15 changed files with 5320 additions and 121 deletions

View 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}</>;
}

View 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>;
}