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:
@@ -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
99
src/store/permissions.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user