fix and update

This commit is contained in:
lendry
2026-07-01 23:27:48 +03:00
parent 2b88e028c6
commit bcdfbc3861
36 changed files with 1504 additions and 320 deletions

View File

@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Ban, BadgeCheck, Crown, FileText, KeyRound, Loader2, ScrollText, Search, ShieldOff, ShieldPlus, UserCheck, UserCog } from 'lucide-react';
import { Ban, BadgeCheck, Crown, FileText, KeyRound, Loader2, MoreVertical, ScrollText, Search, ShieldOff, ShieldPlus, UserCheck, UserCog } from 'lucide-react';
import { UserInspectorDialog } from '@/components/admin/user-inspector-dialog';
import { VerificationBadge } from '@/components/id/verification-badge';
import { UserDocumentsDialog } from '@/components/documents/user-documents-dialog';
@@ -10,10 +10,16 @@ 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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { AdminPermission, AdminRole, AdminUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus, getApiErrorMessage } from '@/lib/api';
import { AdminPermission, AdminRole, AdminUser, PublicUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus, getApiErrorMessage } from '@/lib/api';
import { getAdminLandingPath } from '@/lib/admin-access';
import { DEFAULT_VERIFICATION_ICON, VERIFICATION_ICON_OPTIONS } from '@/lib/verification-icons';
@@ -34,6 +40,113 @@ const roleLabels: Record<string, string> = {
const DEFAULT_USER_ROLE = 'user';
function UserActionsMenu({
user,
currentUser,
actionLoading,
onOpenInspector,
onResetPassword,
onSuspend,
onUnsuspend,
onOpenDocuments,
onDisableTotp,
onOpenVerification,
onOpenRoles,
onToggleSuperAdmin
}: {
user: AdminUser;
currentUser: PublicUser | null;
actionLoading: boolean;
onOpenInspector: () => void;
onResetPassword: () => void;
onSuspend: () => void;
onUnsuspend: () => void;
onOpenDocuments: () => void;
onDisableTotp: () => void;
onOpenVerification: () => void;
onOpenRoles: () => void;
onToggleSuperAdmin: () => void;
}) {
const canInspect = (currentUser?.canViewUsers || currentUser?.canManageUsers) && !user.isBot;
const canManage = Boolean(currentUser?.canManageUsers);
const canDocuments = Boolean(currentUser?.canViewUserDocuments && !user.isBot);
const canDisableTotp = Boolean(currentUser?.isSuperAdmin && !user.isBot);
const canVerify = Boolean(currentUser?.canVerifyUsers);
const canRoles = Boolean(currentUser?.canManageRoles);
if (!canInspect && !canManage && !canDocuments && !canDisableTotp && !canVerify && !canRoles) {
return null;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" aria-label="Действия с пользователем" disabled={actionLoading}>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 rounded-xl">
{canInspect ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenInspector}>
<ScrollText className="h-4 w-4" />
Журнал и чаты
</DropdownMenuItem>
) : null}
{canManage ? (
<>
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onResetPassword}>
<KeyRound className="h-4 w-4" />
Сбросить пароль
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 rounded-lg" disabled={user.status === 'SUSPENDED'} onClick={onSuspend}>
<Ban className="h-4 w-4" />
Заблокировать
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 rounded-lg" disabled={user.status !== 'SUSPENDED'} onClick={onUnsuspend}>
<UserCheck className="h-4 w-4" />
Разблокировать
</DropdownMenuItem>
</>
) : null}
{canDocuments ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenDocuments}>
<FileText className="h-4 w-4" />
Документы
</DropdownMenuItem>
) : null}
{canDisableTotp ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onDisableTotp}>
<ShieldOff className="h-4 w-4" />
Отключить 2FA
</DropdownMenuItem>
) : null}
{canVerify ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenVerification}>
<BadgeCheck className="h-4 w-4" />
{user.isVerified ? 'Изменить верификацию' : 'Верифицировать'}
</DropdownMenuItem>
) : null}
{canRoles ? (
<>
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenRoles}>
<UserCog className="h-4 w-4" />
Роли и доступ
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2 rounded-lg"
disabled={user.id === currentUser?.id}
onClick={onToggleSuperAdmin}
>
<Crown className="h-4 w-4" />
{user.isSuperAdmin ? 'Снять супер-админа' : 'Назначить супер-админом'}
</DropdownMenuItem>
</>
) : null}
</DropdownMenuContent>
</DropdownMenu>
);
}
export default function AdminUsersPage() {
const router = useRouter();
const { token, user: currentUser } = useAuth();
@@ -367,98 +480,28 @@ export default function AdminUsersPage() {
<span className="rounded-full bg-[#f4f5f8] px-3 py-1 text-xs font-semibold">{statusLabels[user.status] ?? user.status}</span>
</TableCell>
<TableCell>
<div className="flex justify-end gap-2" onClick={(event) => event.stopPropagation()}>
{(currentUser?.canViewUsers || currentUser?.canManageUsers) && !user.isBot ? (
<Button
variant="secondary"
size="icon"
aria-label="Журнал и чаты"
onClick={() => setInspectorUser(user)}
>
<ScrollText className="h-4 w-4" />
</Button>
) : null}
{currentUser?.canManageUsers ? (
<>
<Button
variant="secondary"
size="icon"
aria-label="Сбросить пароль"
disabled={actionLoading}
onClick={() => {
setSelectedUser(user);
setPassword('');
setDialog('password');
}}
>
<KeyRound className="h-4 w-4" />
</Button>
<Button variant="secondary" size="icon" aria-label="Заблокировать" disabled={actionLoading || user.status === 'SUSPENDED'} onClick={() => void handleSuspend(user)}>
<Ban className="h-4 w-4" />
</Button>
<Button variant="secondary" size="icon" aria-label="Разблокировать" disabled={actionLoading || user.status !== 'SUSPENDED'} onClick={() => void handleUnsuspend(user)}>
<UserCheck className="h-4 w-4" />
</Button>
</>
) : null}
{currentUser?.canViewUserDocuments && !user.isBot ? (
<Button
variant="secondary"
size="icon"
aria-label="Документы пользователя"
disabled={actionLoading}
onClick={() => setDocumentsUser(user)}
>
<FileText className="h-4 w-4" />
</Button>
) : null}
{currentUser?.isSuperAdmin && !user.isBot ? (
<Button
variant="secondary"
size="icon"
aria-label="Отключить 2FA"
disabled={actionLoading}
onClick={() => void handleAdminDisableTotp(user)}
>
<ShieldOff className="h-4 w-4" />
</Button>
) : null}
{currentUser?.canVerifyUsers ? (
<Button
variant={user.isVerified ? 'default' : 'secondary'}
size="icon"
aria-label="Верификация"
disabled={actionLoading}
onClick={() => openVerificationDialog(user)}
>
<BadgeCheck className="h-4 w-4" />
</Button>
) : null}
{currentUser?.canManageRoles ? (
<>
<Button
variant="secondary"
size="icon"
aria-label="Управление ролями"
disabled={actionLoading}
onClick={() => {
setSelectedUser(user);
setDialog('roles');
}}
>
<UserCog className="h-4 w-4" />
</Button>
<Button
variant={user.isSuperAdmin ? 'default' : 'secondary'}
size="icon"
aria-label="Супер-админ"
disabled={actionLoading || user.id === currentUser?.id}
onClick={() => void handleToggleSuperAdmin(user)}
>
<Crown className="h-4 w-4" />
</Button>
</>
) : null}
<div className="flex justify-end" onClick={(event) => event.stopPropagation()}>
<UserActionsMenu
user={user}
currentUser={currentUser}
actionLoading={actionLoading}
onOpenInspector={() => setInspectorUser(user)}
onResetPassword={() => {
setSelectedUser(user);
setPassword('');
setDialog('password');
}}
onSuspend={() => void handleSuspend(user)}
onUnsuspend={() => void handleUnsuspend(user)}
onOpenDocuments={() => setDocumentsUser(user)}
onDisableTotp={() => void handleAdminDisableTotp(user)}
onOpenVerification={() => openVerificationDialog(user)}
onOpenRoles={() => {
setSelectedUser(user);
setDialog('roles');
}}
onToggleSuperAdmin={() => void handleToggleSuperAdmin(user)}
/>
</div>
</TableCell>
</TableRow>