From 5117878185f75fffe83c29c5eb015e9bf8ffc2d1 Mon Sep 17 00:00:00 2001 From: smanylov Date: Tue, 19 May 2026 16:54:20 +0700 Subject: [PATCH] add permission check --- package.json | 2 +- .../[locale]/pages/staff-management/page.tsx | 3 - src/app/actions/auth.ts | 6 +- src/app/actions/session.ts | 38 ++++- src/app/actions/stuffActions.ts | 39 ++++- src/app/hooks/react-query/useEmployeInfo.ts | 153 ++++++++++++++++++ .../staff-management/tanstak-stuff-table.tsx | 4 +- src/app/ui/users/users-table.tsx | 10 +- 8 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 src/app/hooks/react-query/useEmployeInfo.ts diff --git a/package.json b/package.json index 521bd6e..6f062c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "no-copy-admin-panel-frontend", - "version": "0.8.0", + "version": "0.9.0", "private": true, "scripts": { "dev": "next dev -p 2996", diff --git a/src/app/[locale]/pages/staff-management/page.tsx b/src/app/[locale]/pages/staff-management/page.tsx index af3f943..0a150f0 100644 --- a/src/app/[locale]/pages/staff-management/page.tsx +++ b/src/app/[locale]/pages/staff-management/page.tsx @@ -1,9 +1,6 @@ 'use client' -import { useStuffList } from '@/app/hooks/react-query/stuff/useStuffList'; -import { useEffect } from 'react'; import TanstakStuffTable from '@/app/ui/staff-management/tanstak-stuff-table'; -import CreateStaffForm from '@/app/ui/forms/create-staff-form'; import StaffManagement from '@/app/ui/staff-management/staff-add-management'; export default function Page() { diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index 1aa2cc4..dd4eee6 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -18,8 +18,8 @@ export async function authorization( /* для теста */ if (email === 'test' && password === 'test') { - await createSession('1111', 'test'); - redirect('/pages/'); + /* await createSession('1111', 'test', 0); */ + /* redirect('/pages/'); */ } /* для теста */ @@ -70,7 +70,7 @@ export async function authorization( if (response.ok) { let parsed = await response.json(); if (parsed.message_code === 0) { - await createSession(parsed.message_body.token, email); + await createSession(parsed.message_body.token, email, parsed.message_body.admin.id); } else { throw (`${parsed.message_code}`); } diff --git a/src/app/actions/session.ts b/src/app/actions/session.ts index 4f0bca6..4677a09 100644 --- a/src/app/actions/session.ts +++ b/src/app/actions/session.ts @@ -4,11 +4,33 @@ import { cookies } from 'next/headers'; import { SignJWT, jwtVerify } from 'jose'; import { redirect } from 'next/navigation'; -import { headers } from 'next/headers'; /* const secretKey = process.env.SESSION_SECRET */ -const secretKey = 'AAA+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo=' -const encodedKey = new TextEncoder().encode(secretKey) +const secretKey = process.env.SESSION_SECRET || 'AAA+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo='; +const encodedKey = new TextEncoder().encode(secretKey); + +export interface SessionPayload { + token: string; + email: string; + employeId: number; + permissions: { + subscriptions: number; + mailings: number; + complaints: number; + agreements: number; + staff: number; + content_moderation: number; + users: number; + kyc: number; + money: number; + claims: number; + api: number; + dash: number; + tariffs: number; + }; + expiresAt: Date | number; + createdAt?: Date; +} export async function encrypt(payload: any) { return new SignJWT(payload) @@ -29,18 +51,20 @@ export async function decrypt(session: string | undefined = '') { } } -export async function createSession(token: string, email: string) { +export async function createSession(token: string, email: string, employeId: number) { const expiresAt = new Date(Date.now() + 50 * 60 * 1000); const session = await encrypt({ token, email, - expiresAt + employeId, + expiresAt, + createdAt: new Date() }); const cookieStore = await cookies(); cookieStore.set('session_nc_ap', session, { httpOnly: true, - secure: false, + secure: process.env.NODE_ENV !== 'development', expires: expiresAt, sameSite: 'lax', path: '/', @@ -78,7 +102,7 @@ export async function updateSession(newToken: string) { }); } -export async function getSessionData(type: 'token' | 'email'): Promise { +export async function getSessionData(type: 'token' | 'email' | 'employeId'): Promise { const cookieStore = await cookies(); const sessionCookie = cookieStore.get('session_nc_ap')?.value; diff --git a/src/app/actions/stuffActions.ts b/src/app/actions/stuffActions.ts index bcbb012..e7ed155 100644 --- a/src/app/actions/stuffActions.ts +++ b/src/app/actions/stuffActions.ts @@ -14,7 +14,6 @@ export async function fetchStuffList(page?: number, size?: number, sortBy?: stri msg_id: 100, token: token, message_body: { - /* token: token, */ action: 'get_all', page: page, size: size, @@ -169,6 +168,44 @@ export async function updateStuffPermissions(id: number, permissions: Permission } }); + if (response.ok) { + const parsed = await response.json(); + + if (parsed.message_code === 0) { + return parsed.message_body; + } else { + return null; + } + } else { + throw new Error(`${response.status}`); + } + } catch (error) { + throw error; + } +} + +export async function fetchEmployeInfo() { + const token = await getSessionData('token'); + const employeId = await getSessionData('employeId'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 100, + token: token, + message_body: { + action: 'get_by_id', + admin_id: employeId + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + if (response.ok) { const parsed = await response.json(); diff --git a/src/app/hooks/react-query/useEmployeInfo.ts b/src/app/hooks/react-query/useEmployeInfo.ts new file mode 100644 index 0000000..263142b --- /dev/null +++ b/src/app/hooks/react-query/useEmployeInfo.ts @@ -0,0 +1,153 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { fetchEmployeInfo } from '@/app/actions/stuffActions'; + +export type PermissionKey = + | 'subscriptions' + | 'mailings' + | 'complaints' + | 'agreements' + | 'staff' + | 'content_moderation' + | 'users' + | 'kyc' + | 'money' + | 'claims' + | 'api' + | 'dash' + | 'tariffs'; + +export type PermissionLevel = 0 | 1 | 3; + +export interface UserData { + id: number; + full_name: string; + email: string; + is_active: boolean; + is_super_admin: boolean; + created_at: string; + active_password: boolean; + permissions: Record; +} + +interface UseEmployeInfoReturn { + data: UserData | null; + isLoading: boolean; + isError: boolean; + error: Error | null; + isFetching: boolean; + refetch: () => void; + + hasPermission: (key: PermissionKey, requiredLevel?: PermissionLevel) => boolean; + getPermissionLevel: (key: PermissionKey) => PermissionLevel; + canRead: (key: PermissionKey) => boolean; + canFullAccess: (key: PermissionKey) => boolean; + + hasAllPermissions: (permissions: Partial>) => boolean; + hasAnyPermission: (permissions: Partial>) => boolean; + + getPermissionsArray: () => Array<{ key: PermissionKey; level: PermissionLevel; label: string }>; + + isSuperAdmin: boolean; +} + +const PERMISSION_LABELS: Record = { + subscriptions: 'Подписки', + mailings: 'Рассылки', + complaints: 'Жалобы', + agreements: 'Соглашения', + staff: 'Сотрудники', + content_moderation: 'Модерация контента', + users: 'Пользователи', + kyc: 'Верификация', + money: 'Финансы', + claims: 'Претензии', + api: 'API', + dash: 'Дешборд', + tariffs: 'Тарифы' +}; + +export const useEmployeInfo = (options?: UseQueryOptions) => { + const query = useQuery({ + queryKey: ['employeInfo'], + queryFn: () => fetchEmployeInfo(), + select: (data: UserData | null) => data, + staleTime: 5 * 60 * 1000, + gcTime: 5 * 60 * 1000, + retry: 1, + refetchOnWindowFocus: true, + refetchOnMount: true, + ...options + }); + + const permissions = query.data?.permissions || null; + + const hasPermission = (key: PermissionKey, requiredLevel: PermissionLevel = 1): boolean => { + if (!permissions) return false; + const userLevel = permissions[key] || 0; + return userLevel >= requiredLevel; + }; + + const getPermissionLevel = (key: PermissionKey): PermissionLevel => { + if (!permissions) return 0; + const level = permissions[key] || 0; + if (level === 3) return 3; + if (level === 1) return 1; + return 0; + }; + + const canRead = (key: PermissionKey): boolean => hasPermission(key, 1); + const canFullAccess = (key: PermissionKey): boolean => hasPermission(key, 3); + + const hasAllPermissions = (requiredPermissions: Partial>): boolean => { + if (!permissions) return false; + + for (const [key, requiredLevel] of Object.entries(requiredPermissions)) { + const userLevel = permissions[key as PermissionKey] || 0; + if (userLevel < (requiredLevel as PermissionLevel)) { + return false; + } + } + return true; + }; + + const hasAnyPermission = (requiredPermissions: Partial>): boolean => { + if (!permissions) return false; + + for (const [key, requiredLevel] of Object.entries(requiredPermissions)) { + const userLevel = permissions[key as PermissionKey] || 0; + if (userLevel >= (requiredLevel as PermissionLevel)) { + return true; + } + } + return false; + }; + + const getPermissionsArray = (): Array<{ key: PermissionKey; level: PermissionLevel; label: string }> => { + if (!permissions) return []; + + return (Object.keys(PERMISSION_LABELS) as PermissionKey[]).map(key => ({ + key, + level: permissions[key] || 0, + label: PERMISSION_LABELS[key] + })); + }; + + return { + data: query.data, + isLoading: query.isLoading, + isError: query.isError, + error: query.error as Error | null, + isFetching: query.isFetching, + refetch: query.refetch, + + hasPermission, + getPermissionLevel, + canRead, + canFullAccess, + + hasAllPermissions, + hasAnyPermission, + getPermissionsArray, + isSuperAdmin: query.data?.is_super_admin || false, + }; +}; \ No newline at end of file diff --git a/src/app/ui/staff-management/tanstak-stuff-table.tsx b/src/app/ui/staff-management/tanstak-stuff-table.tsx index f681425..74a598b 100644 --- a/src/app/ui/staff-management/tanstak-stuff-table.tsx +++ b/src/app/ui/staff-management/tanstak-stuff-table.tsx @@ -118,9 +118,8 @@ export default function TanstakStuffTable() { mutationFn: (id: number) => removeStuff(id), onSuccess: () => { toast.success(tStaff('staff-removed-successfully')); - // Инвалидируем кэш списка сотрудников queryClient.invalidateQueries({ queryKey: ['stuffList'] }); - setConfirmRemove(null); // Закрываем окно подтверждения + setConfirmRemove(null); }, onError: (error: Error) => { toast.error(tStaff(error.message || 'removal-failed')); @@ -134,6 +133,7 @@ export default function TanstakStuffTable() { onSuccess: () => { toast.success(tStaff('permissions-updated-successfully')); queryClient.invalidateQueries({ queryKey: ['stuffList'] }); + queryClient.invalidateQueries({ queryKey: ['employeInfo'] }); setOpenWindow(false); }, onError: (error: Error) => { diff --git a/src/app/ui/users/users-table.tsx b/src/app/ui/users/users-table.tsx index 58d974e..72ff10b 100644 --- a/src/app/ui/users/users-table.tsx +++ b/src/app/ui/users/users-table.tsx @@ -1,8 +1,16 @@ +'use client' + import TanstakFilesTable from '@/app/ui/users/tanstak-users-table'; +import { useEmployeInfo } from '@/app/hooks/react-query/useEmployeInfo'; + export function UsersTable() { + const { canRead } = useEmployeInfo(); + return (
- + {canRead('users') && ( + + )}
) } \ No newline at end of file