From 6d512fb0ad884f578af4416c95ed0abdb2270254 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Wed, 23 Sep 2026 21:16:41 +0900 Subject: [PATCH] =?UTF-8?q?fix(resource/api-keys):=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=90=8E=E5=A4=8D=E5=88=B6=E5=BD=93=E5=89=8D=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E5=B9=B6=E6=B8=85=E7=A9=BA=E6=97=A7=E6=98=BE=E7=A4=BA=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard/ApiKeysPanel.test.ts | 21 +++++++++++++++++++ src/components/dashboard/ApiKeysPanel.tsx | 15 ++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/components/dashboard/ApiKeysPanel.test.ts diff --git a/src/components/dashboard/ApiKeysPanel.test.ts b/src/components/dashboard/ApiKeysPanel.test.ts new file mode 100644 index 0000000..2ffcbde --- /dev/null +++ b/src/components/dashboard/ApiKeysPanel.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { copyCurrentApiKey } from "./ApiKeysPanel"; + +describe("copyCurrentApiKey", () => { + it("reads the current credential instead of reusing a previously revealed value", async () => { + let current = "synthetic-old-key"; + let copied = ""; + const reveal = async (id: string) => { + expect(id).toBe("k1"); + return current; + }; + const write = async (value: string) => { copied = value; }; + + await copyCurrentApiKey("k1", reveal, write); + expect(copied).toBe("synthetic-old-key"); + + current = "synthetic-new-key"; + await copyCurrentApiKey("k1", reveal, write); + expect(copied).toBe("synthetic-new-key"); + }); +}); diff --git a/src/components/dashboard/ApiKeysPanel.tsx b/src/components/dashboard/ApiKeysPanel.tsx index 68b675e..d1dd97a 100644 --- a/src/components/dashboard/ApiKeysPanel.tsx +++ b/src/components/dashboard/ApiKeysPanel.tsx @@ -18,6 +18,14 @@ const PROVIDERS = [ { value: "custom", label: "自定义", color: "bg-gray-100 text-gray-600 border-gray-200" }, ]; +export async function copyCurrentApiKey( + id: string, + reveal: (id: string) => Promise, + write: (value: string) => Promise, +): Promise { + await write(await reveal(id)); +} + function getProviderStyle(provider: string) { return PROVIDERS.find((p) => p.value === provider)?.color ?? PROVIDERS[PROVIDERS.length - 1].color; } @@ -144,7 +152,7 @@ function ApiKeyCard({ item, onEdit, onDelete }: { }; const copy = async () => { try { - await navigator.clipboard.writeText(secret ?? await revealApiKey(item.id)); + await copyCurrentApiKey(item.id, revealApiKey, (value) => navigator.clipboard.writeText(value)); setError(undefined); } catch (cause) { setError(String(cause)); @@ -249,6 +257,7 @@ export function ApiKeysPanel() { const [showModal, setShowModal] = useState(false); const [editingItem, setEditingItem] = useState<(ApiKey & { api_key: string }) | null>(null); const [editError, setEditError] = useState(); + const [keyRevision, setKeyRevision] = useState(0); const { data: keys = [] } = useQuery({ queryKey: ["api-keys"], @@ -262,7 +271,7 @@ export function ApiKeysPanel() { const editMut = useMutation({ mutationFn: ({ id, input }: { id: string; input: SaveApiKeyInput }) => editApiKey(id, input), - onSuccess: () => { qc.invalidateQueries({ queryKey: ["api-keys"] }); setEditingItem(null); }, + onSuccess: () => { setKeyRevision((revision) => revision + 1); qc.invalidateQueries({ queryKey: ["api-keys"] }); setEditingItem(null); }, }); const deleteMut = useMutation({ @@ -299,7 +308,7 @@ export function ApiKeysPanel() { {keys.map((k) => ( { void (async () => { try {