fix and update
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user