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

@@ -6,6 +6,7 @@ import { apiClient } from '../api/client';
import { captureCampaignFromUrl, consumeCampaignSlug } from '../utils/campaign';
import { captureReferralFromUrl, consumeReferralCode } from '../utils/referral';
import { tokenStorage, isTokenValid, tokenRefreshManager } from '../utils/token';
import { usePermissionStore } from './permissions';
export interface TelegramWidgetData {
id: number;
@@ -93,6 +94,7 @@ export const useAuthStore = create<AuthState>()(
});
}
tokenStorage.clearTokens();
usePermissionStore.getState().reset();
set({
accessToken: null,
refreshToken: null,
@@ -107,13 +109,20 @@ export const useAuthStore = create<AuthState>()(
const token = tokenStorage.getAccessToken();
if (!token || !isTokenValid(token)) {
set({ isAdmin: false });
usePermissionStore.getState().reset();
return;
}
// Используем apiClient для единообразной обработки ошибок
const response = await apiClient.get<{ is_admin: boolean }>('/cabinet/auth/me/is-admin');
set({ isAdmin: response.data.is_admin });
if (response.data.is_admin) {
await usePermissionStore.getState().fetchPermissions();
} else {
usePermissionStore.getState().reset();
}
} catch {
set({ isAdmin: false });
usePermissionStore.getState().reset();
}
},

99
src/store/permissions.ts Normal file
View File

@@ -0,0 +1,99 @@
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,
});
},
}));