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 {