'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Ban, BadgeCheck, Crown, FileText, KeyRound, Loader2, Search, ShieldPlus, UserCheck, UserCog } from 'lucide-react'; import { VerificationBadge } from '@/components/id/verification-badge'; import { UserDocumentsDialog } from '@/components/documents/user-documents-dialog'; import { AdminShell } from '@/components/id/admin-shell'; import { useAuth } from '@/components/id/auth-provider'; import { useToast } from '@/components/id/toast-provider'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { AdminPermission, AdminRole, AdminUser, apiFetch } from '@/lib/api'; import { getAdminLandingPath } from '@/lib/admin-access'; import { DEFAULT_VERIFICATION_ICON, VERIFICATION_ICON_OPTIONS } from '@/lib/verification-icons'; const statusLabels: Record = { ACTIVE: 'Активен', SUSPENDED: 'Заблокирован', DELETED: 'Удалён' }; const roleLabels: Record = { user: 'Пользователь', admin: 'Администратор', moderator: 'Модератор', manager: 'Менеджер', 'super-admin': 'Супер-админ' }; const DEFAULT_USER_ROLE = 'user'; export default function AdminUsersPage() { const router = useRouter(); const { token, user: currentUser } = useAuth(); const { showToast } = useToast(); const [users, setUsers] = useState([]); const [roles, setRoles] = useState([]); const [permissions, setPermissions] = useState([]); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); const [selectedUser, setSelectedUser] = useState(null); const [password, setPassword] = useState(''); const [actionLoading, setActionLoading] = useState(false); const [dialog, setDialog] = useState<'password' | 'roles' | 'verification' | null>(null); const [documentsUser, setDocumentsUser] = useState(null); const [verificationIcon, setVerificationIcon] = useState(DEFAULT_VERIFICATION_ICON); const loadUsers = useCallback(async () => { if (!token) return; setLoading(true); try { const response = await apiFetch<{ users: AdminUser[] }>(`/admin/users${search ? `?search=${encodeURIComponent(search)}` : ''}`, {}, token); setUsers(response.users ?? []); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось загрузить пользователей'); } finally { setLoading(false); } }, [search, showToast, token]); useEffect(() => { if (!currentUser) return; if (!currentUser.canViewUsers && !currentUser.canManageUsers) { router.replace(getAdminLandingPath(currentUser)); } }, [currentUser, router]); useEffect(() => { if (!currentUser?.canViewUsers && !currentUser?.canManageUsers) return; void loadUsers(); }, [currentUser?.canManageUsers, currentUser?.canViewUsers, loadUsers]); useEffect(() => { if (!token || !currentUser?.canManageRoles) return; Promise.all([ apiFetch<{ roles: AdminRole[] }>('/admin/rbac/roles', {}, token), apiFetch<{ permissions: AdminPermission[] }>('/admin/rbac/permissions', {}, token) ]) .then(([rolesResponse, permissionsResponse]) => { setRoles(rolesResponse.roles ?? []); setPermissions(permissionsResponse.permissions ?? []); }) .catch(() => undefined); }, [currentUser?.canManageRoles, token]); const assignableRoles = useMemo( () => roles.filter((role) => role.slug !== 'super-admin' && !role.isDefault), [roles] ); const permissionLabelBySlug = useMemo( () => Object.fromEntries(permissions.map((permission) => [permission.slug, permission.name])), [permissions] ); async function handleSuspend(user: AdminUser) { if (!token) return; setActionLoading(true); try { await apiFetch(`/admin/users/${user.id}/suspend`, { method: 'POST' }, token); showToast('Пользователь заблокирован'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось заблокировать пользователя'); } finally { setActionLoading(false); } } async function handleUnsuspend(user: AdminUser) { if (!token) return; setActionLoading(true); try { await apiFetch(`/admin/users/${user.id}`, { method: 'PATCH', body: JSON.stringify({ status: 'ACTIVE' }) }, token); showToast('Пользователь разблокирован'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось разблокировать пользователя'); } finally { setActionLoading(false); } } async function handleResetPassword() { if (!token || !selectedUser || password.length < 8) return; setActionLoading(true); try { await apiFetch(`/admin/users/${selectedUser.id}/reset-password`, { method: 'POST', body: JSON.stringify({ newPassword: password }) }, token); showToast('Пароль обновлён'); setDialog(null); setPassword(''); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось сбросить пароль'); } finally { setActionLoading(false); } } async function handleToggleSuperAdmin(user: AdminUser) { if (!token || !currentUser?.canManageRoles) return; setActionLoading(true); try { await apiFetch(`/admin/users/${user.id}/super-admin`, { method: 'PATCH', body: JSON.stringify({ isSuperAdmin: !user.isSuperAdmin }) }, token); showToast(user.isSuperAdmin ? 'Права супер-админа сняты' : 'Пользователь назначен супер-админом'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось изменить права'); } finally { setActionLoading(false); } } async function handleAssignRole(roleSlug: string) { if (!token || !selectedUser) return; setActionLoading(true); try { const response = await apiFetch<{ roles: string[] }>(`/admin/rbac/users/${selectedUser.id}/roles`, { method: 'POST', body: JSON.stringify({ roleSlug }) }, token); setSelectedUser({ ...selectedUser, roles: response.roles ?? [] }); showToast('Роль назначена'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось назначить роль'); } finally { setActionLoading(false); } } async function handleRemoveRole(roleSlug: string) { if (!token || !selectedUser) return; setActionLoading(true); try { await apiFetch(`/admin/rbac/users/${selectedUser.id}/roles/${roleSlug}`, { method: 'DELETE' }, token); setSelectedUser({ ...selectedUser, roles: (selectedUser.roles ?? []).filter((role) => role !== roleSlug) }); showToast('Роль снята'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось снять роль'); } finally { setActionLoading(false); } } async function handleAssignPermission(permissionSlug: string) { if (!token || !selectedUser) return; setActionLoading(true); try { const response = await apiFetch<{ permissions: string[] }>(`/admin/rbac/users/${selectedUser.id}/permissions`, { method: 'POST', body: JSON.stringify({ permissionSlug }) }, token); setSelectedUser({ ...selectedUser, directPermissions: response.permissions ?? [] }); showToast('Право назначено'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось назначить право'); } finally { setActionLoading(false); } } async function handleRemovePermission(permissionSlug: string) { if (!token || !selectedUser) return; setActionLoading(true); try { const response = await apiFetch<{ permissions: string[] }>(`/admin/rbac/users/${selectedUser.id}/permissions/${permissionSlug}`, { method: 'DELETE' }, token); setSelectedUser({ ...selectedUser, directPermissions: response.permissions ?? [] }); showToast('Право снято'); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось снять право'); } finally { setActionLoading(false); } } async function handleSaveVerification() { if (!token || !selectedUser) return; setActionLoading(true); try { const updated = await apiFetch(`/admin/users/${selectedUser.id}/verification`, { method: 'PATCH', body: JSON.stringify({ isVerified: true, verificationIcon }) }, token); showToast('Пользователь верифицирован'); setSelectedUser(updated); setDialog(null); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось верифицировать пользователя'); } finally { setActionLoading(false); } } async function handleRemoveVerification() { if (!token || !selectedUser) return; setActionLoading(true); try { await apiFetch(`/admin/users/${selectedUser.id}/verification`, { method: 'PATCH', body: JSON.stringify({ isVerified: false }) }, token); showToast('Верификация снята'); setSelectedUser({ ...selectedUser, isVerified: false, verificationIcon: undefined }); setDialog(null); await loadUsers(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось снять верификацию'); } finally { setActionLoading(false); } } function openVerificationDialog(user: AdminUser) { setSelectedUser(user); setVerificationIcon(user.verificationIcon ?? DEFAULT_VERIFICATION_ICON); setDialog('verification'); } function renderRoles(user: AdminUser) { const userRoles = user.roles ?? []; const extraRoles = userRoles.filter((role) => role !== DEFAULT_USER_ROLE); const direct = user.directPermissions ?? []; const labels = user.isSuperAdmin ? ['Супер-админ', ...extraRoles.map((role) => roleLabels[role] ?? role)] : extraRoles.map((role) => roleLabels[role] ?? role); const directLabels = direct.map((slug) => permissionLabelBySlug[slug] ?? slug); const parts = [...labels, ...directLabels.map((label) => `+ ${label}`)]; if (!parts.length) return roleLabels.user; return parts.join(', '); } return (

Пользователи

Поиск, блокировка, сброс пароля и управление доступом

setSearch(event.target.value)} placeholder="Поиск по имени, почте или телефону" className="pl-10" />
{loading ? (
Загрузка...
) : ( Пользователь Телефон Роли Статус Действия {users.map((user) => (
{user.displayName} {user.isVerified ? : null}
{user.email ?? user.username ?? '—'}
{user.phone ?? '—'} {renderRoles(user)} {statusLabels[user.status] ?? user.status}
{currentUser?.canManageUsers ? ( <> ) : null} {currentUser?.canViewUserDocuments ? ( ) : null} {currentUser?.canVerifyUsers ? ( ) : null} {currentUser?.canManageRoles ? ( <> ) : null}
))}
)}
!open && setDialog(null)}> Сброс пароля

Новый пароль для {selectedUser?.displayName}

setPassword(event.target.value)} placeholder="Минимум 8 символов" />
!open && setDialog(null)}> Роли и доступ

{selectedUser?.displayName}

{(selectedUser?.roles ?? []).map((role) => (
{roleLabels[role] ?? role}{role === DEFAULT_USER_ROLE ? ' (стандартная)' : ''} {role === DEFAULT_USER_ROLE ? ( Нельзя снять ) : ( )}
))} {!(selectedUser?.roles ?? []).length ?

Роли не назначены

: null}

Назначить роль

{assignableRoles.map((role) => ( ))}

Прямые права

Дополнительные permissions без смены роли. Например, oauth.manage для одного пользователя.

{(selectedUser?.directPermissions ?? []).map((permissionSlug) => (
{permissionLabelBySlug[permissionSlug] ?? permissionSlug}
))} {!(selectedUser?.directPermissions ?? []).length ? (

Прямые права не назначены

) : null}
{permissions .filter((permission) => !(selectedUser?.directPermissions ?? []).includes(permission.slug)) .map((permission) => ( ))}
!open && setDialog(null)}> Верификация пользователя

{selectedUser?.displayName}

{selectedUser?.isVerified ? (
Пользователь уже верифицирован
) : null}

Выберите значок

{VERIFICATION_ICON_OPTIONS.map((option) => ( ))}
{selectedUser?.isVerified ? ( ) : null}
{documentsUser ? ( { if (!open) setDocumentsUser(null); }} targetUserId={documentsUser.id} targetUserName={documentsUser.displayName} token={token} /> ) : null}
); }