import { type ApiKey, ApiKeyCreate, type ApiScope } from "@umt/contracts"; import { Check, Copy, LoaderCircle, Plus } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { useApiKeys, useCreateApiKey, useRevokeApiKey } from "~/api/api-keys.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { DataTable } from "~/components/console/data-table.tsx"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog.tsx"; import { Input } from "~/components/ui/input.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatRelative } from "~/lib/labels.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import { SettingsBlock } from "./settings-block.tsx"; // ─── API Key ─── const SCOPES: { value: ApiScope; label: string; hint: string }[] = [ { value: "read", label: "只读 · read", hint: "只读查询全部数据" }, { value: "organize.read", label: "读整理记录 · organize.read", hint: "只读整理记录,供外部增量同步" }, { value: "organize.execute", label: "执行整理 · organize.execute", hint: "发起整理" }, { value: "admin", label: "管理员 · admin", hint: "全部管理权限,含改设置" }, ]; function CreatedKeyDialog({ plaintext, onClose }: { plaintext: string | null; onClose: () => void }) { const [copied, setCopied] = useState(false); return ( !o && onClose()}> API Key 已创建 明文只显示这一次,关闭后无法再查看。
{plaintext}
); } export function ApiKeysSection() { const keys = useApiKeys(); const create = useCreateApiKey(); const revoke = useRevokeApiKey(); const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [scopes, setScopes] = useState(["read"]); const [errors, setErrors] = useState>({}); const [plaintext, setPlaintext] = useState(null); const [revoking, setRevoking] = useState(null); const submit = async (e: React.FormEvent) => { e.preventDefault(); const parsed = ApiKeyCreate.safeParse({ name: name.trim(), scopes }); if (!parsed.success) { const fe = zodFieldErrors(parsed.error.issues); if (fe.name) fe.name = "名称必填,80 字以内"; if (fe.scopes) fe.scopes = "至少选一个权限范围"; setErrors(fe); return; } setErrors({}); try { const r = await create.mutateAsync(parsed.data); setOpen(false); setName(""); setScopes(["read"]); setPlaintext(r.plaintextKey); } catch (err) { toast.error(errorMessage(err, "创建失败")); } }; return ( setOpen(true)}> 创建 API Key } > r.id} loading={keys.isPending} error={keys.error} onRetry={() => keys.refetch()} empty={{ title: "还没有 API Key" }} columns={[ { key: "name", header: "名称", cell: (r) => (

{r.name}

创建于 {formatDateTime(r.createdAt)}

), }, { key: "scopes", header: "权限", hideBelow: "sm", cell: (r) => (
{r.scopes.map((s) => ( {s} ))}
), }, { key: "used", header: "最近使用", hideBelow: "md", cell: (r) => formatRelative(r.lastUsedAt) }, { key: "status", header: "状态", cell: (r) => ( {r.revokedAt ? "已撤销" : "有效"} ), }, { key: "act", header: 操作, align: "right", cell: (r) => r.revokedAt ? null : ( ), }, ]} /> !create.isPending && setOpen(o)}> 创建 API Key 创建后明文只显示一次。
setName(e.target.value)} aria-invalid={!!errors.name} placeholder="emby-plugin" />
{SCOPES.map((s) => { const on = scopes.includes(s.value); return ( ); })}
setPlaintext(null)} /> !o && setRevoking(null)} title={`撤销「${revoking?.name ?? ""}」?`} description="使用这个 Key 的程序立即失去访问权限,无法恢复。" confirmLabel="撤销 Key" destructive onConfirm={async () => { if (!revoking) return; try { await revoke.mutateAsync(revoking.id); toast.success("已撤销"); } catch (err) { toast.error(errorMessage(err, "撤销失败")); throw err; } }} />
); } function ScopeLegend() { return (
{SCOPES.map((s) => (
{s.label}
{s.hint}
))}
); }