mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
- 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
100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
import { create } from 'zustand';
|
|
import apiClient from '../api/client';
|
|
|
|
interface PermissionsResponse {
|
|
permissions: string[];
|
|
roles: string[];
|
|
role_level: number;
|
|
}
|
|
|
|
/**
|
|
* Match a user permission against a required permission.
|
|
*
|
|
* - `*:*` matches everything
|
|
* - `users:*` matches `users:read`, `users:edit`, etc.
|
|
* - Exact match otherwise
|
|
*/
|
|
function permissionMatches(userPerm: string, required: string): boolean {
|
|
if (userPerm === '*:*') return true;
|
|
|
|
if (userPerm.endsWith(':*')) {
|
|
const colonIdx = userPerm.indexOf(':');
|
|
const requiredColonIdx = required.indexOf(':');
|
|
if (colonIdx === -1 || requiredColonIdx === -1) return false;
|
|
const section = userPerm.slice(0, colonIdx);
|
|
const requiredSection = required.slice(0, requiredColonIdx);
|
|
return section === requiredSection;
|
|
}
|
|
|
|
return userPerm === required;
|
|
}
|
|
|
|
interface PermissionState {
|
|
permissions: string[];
|
|
roles: string[];
|
|
roleLevel: number;
|
|
isLoaded: boolean;
|
|
|
|
fetchPermissions: () => Promise<void>;
|
|
hasPermission: (permission: string) => boolean;
|
|
hasAnyPermission: (...permissions: string[]) => boolean;
|
|
hasAllPermissions: (...permissions: string[]) => boolean;
|
|
canManageRole: (level: number) => boolean;
|
|
reset: () => void;
|
|
}
|
|
|
|
export const usePermissionStore = create<PermissionState>((set, get) => ({
|
|
permissions: [],
|
|
roles: [],
|
|
roleLevel: 0,
|
|
isLoaded: false,
|
|
|
|
fetchPermissions: async () => {
|
|
try {
|
|
const response = await apiClient.get<PermissionsResponse>('/cabinet/auth/me/permissions');
|
|
set({
|
|
permissions: response.data.permissions,
|
|
roles: response.data.roles,
|
|
roleLevel: response.data.role_level,
|
|
isLoaded: true,
|
|
});
|
|
} catch {
|
|
set({
|
|
permissions: [],
|
|
roles: [],
|
|
roleLevel: 0,
|
|
isLoaded: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
hasPermission: (permission: string): boolean => {
|
|
const { permissions } = get();
|
|
return permissions.some((userPerm) => permissionMatches(userPerm, permission));
|
|
},
|
|
|
|
hasAnyPermission: (...permissions: string[]): boolean => {
|
|
const { hasPermission } = get();
|
|
return permissions.some((perm) => hasPermission(perm));
|
|
},
|
|
|
|
hasAllPermissions: (...permissions: string[]): boolean => {
|
|
const { hasPermission } = get();
|
|
return permissions.every((perm) => hasPermission(perm));
|
|
},
|
|
|
|
canManageRole: (level: number): boolean => {
|
|
const { roleLevel } = get();
|
|
return roleLevel > level;
|
|
},
|
|
|
|
reset: () => {
|
|
set({
|
|
permissions: [],
|
|
roles: [],
|
|
roleLevel: 0,
|
|
isLoaded: false,
|
|
});
|
|
},
|
|
}));
|