'use client'; import * as React from 'react'; import Link from 'next/link'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { motion } from 'motion/react'; import { AlertTriangle, Check, Copy, Info, Key, Loader2, Plus, RefreshCw, Shield, Trash2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import type { CreateTokenResponse } from '@/lib/services/user'; import { UserService } from '@/lib/services/user'; import { useAuth } from '@/components/providers/auth-provider'; import { toast } from 'sonner'; import { useLocale, useTranslations } from 'next-intl'; import { formatDateTime } from '@/i18n/format'; import type { AppLocale } from '@/i18n/config'; type ConfirmTarget = { id: number; name: string }; export function AccessTokenMain() { const { user } = useAuth(); const queryClient = useQueryClient(); const t = useTranslations('settings'); const tCommon = useTranslations('common'); const ta = useTranslations('settings.accessToken'); const locale = useLocale() as AppLocale; const [createDialogOpen, setCreateDialogOpen] = React.useState(false); const [viewDialogOpen, setViewDialogOpen] = React.useState(false); const [tokenName, setTokenName] = React.useState(''); const [tokenIsAdmin, setTokenIsAdmin] = React.useState(false); const [copiedId, setCopiedId] = React.useState(null); const [newCreatedToken, setNewCreatedToken] = React.useState(null); const [deleteTarget, setDeleteTarget] = React.useState( null, ); const [rotateTarget, setRotateTarget] = React.useState( null, ); // 获取 Token 列表 const accessTokensQuery = useQuery({ queryKey: ['user', 'access-tokens'], queryFn: () => UserService.getAccessTokens(), }); // 创建 Token const createTokenMutation = useMutation({ mutationFn: ({ name, isAdmin }: { name: string; isAdmin: boolean }) => UserService.createAccessToken(name, isAdmin), onSuccess: (data) => { setNewCreatedToken(data); setTokenName(''); setTokenIsAdmin(false); setCreateDialogOpen(false); setViewDialogOpen(true); void queryClient.invalidateQueries({ queryKey: ['user', 'access-tokens'], }); toast.success(ta('createSuccess')); }, onError: (error: Error) => { toast.error(error.message || ta('createFailed')); }, }); // 删除 Token const deleteTokenMutation = useMutation({ mutationFn: (id: number) => UserService.deleteAccessToken(id), onSuccess: () => { setDeleteTarget(null); void queryClient.invalidateQueries({ queryKey: ['user', 'access-tokens'], }); toast.success(ta('deleteSuccess')); }, onError: (error: Error) => { toast.error(error.message || ta('deleteFailed')); }, }); // 轮换 Token const rotateTokenMutation = useMutation({ mutationFn: (id: number) => UserService.rotateAccessToken(id), onSuccess: (data) => { setRotateTarget(null); setNewCreatedToken(data); setViewDialogOpen(true); void queryClient.invalidateQueries({ queryKey: ['user', 'access-tokens'], }); toast.success(ta('rotateSuccess')); }, onError: (error: Error) => { toast.error(error.message || ta('rotateFailed')); }, }); const handleCreateToken = (e: React.FormEvent) => { e.preventDefault(); if (!tokenName.trim()) { toast.error(ta('nameRequired')); return; } createTokenMutation.mutate({ name: tokenName.trim(), isAdmin: tokenIsAdmin, }); }; const handleCopyText = async (text: string, id: number) => { try { await navigator.clipboard.writeText(text); setCopiedId(id); toast.success(ta('copySuccess')); setTimeout(() => setCopiedId(null), 2000); } catch { toast.error(ta('copyFailed')); } }; const formatDate = (dateStr?: string) => { if (!dateStr) return '—'; return formatDateTime(dateStr, locale); }; return (
{t('title')} {ta('breadcrumb')}

{ta('title')}

{ta('subtitle')}

{/* 安全警告提示 */}
{ta('securityTitle')}

{ta('securityBody')}

{ta('listTitle')} {ta('listDesc')} {accessTokensQuery.isPending ? (
) : (accessTokensQuery.data ?? []).length > 0 ? (
{(accessTokensQuery.data ?? []).map((token) => (
{token.name} {token.is_admin ? ( {ta('adminBadge')} ) : ( {ta('userToken')} )}
{token.masked_token}
{ta('createdAt', { date: formatDate(token.created_at), })}
))}
) : (
{ta('empty')}
)}
{/* 创建令牌 Dialog */}
{ta('generate')} {ta('createDesc')}
setTokenName(e.target.value)} disabled={createTokenMutation.isPending} className='rounded-xl border border-dashed focus:border-primary focus:ring-0 focus-visible:ring-0' />
{user?.is_admin && (

{ta('adminHint')}

)}
{/* 明文 Token 显示 Dialog (仅显示一次) */} { if (!open) { setNewCreatedToken(null); setViewDialogOpen(false); } }} > {ta('readyTitle')} {newCreatedToken && (
{/* 明文 Token 文本框 */}
{newCreatedToken.token}
{/* 强提示 */}
{ta('importantTitle')}

{ta('readyBody')}

)}
!open && setDeleteTarget(null)} > {ta('deleteConfirmTitle')} {ta('deleteConfirm', { name: deleteTarget?.name ?? '' })} {tCommon('cancel')} deleteTarget && deleteTokenMutation.mutate(deleteTarget.id) } > {deleteTokenMutation.isPending ? ta('revoking') : ta('confirmRevoke')} !open && setRotateTarget(null)} > {ta('rotateConfirmTitle')} {ta('rotateConfirm', { name: rotateTarget?.name ?? '' })} {tCommon('cancel')} rotateTarget && rotateTokenMutation.mutate(rotateTarget.id) } > {rotateTokenMutation.isPending ? ta('rotating') : ta('confirmRotate')}
); }