96ce3f4c53
- Extract use-ssh-keys hook for state management - Extract SSHKeyCreateForm and SSHKeyList components - Slim SshKeysPage from 277 to ~80 lines Quality gates: tsc --noEmit passes, npm run build passes
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { useState } from "react";
|
|
import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh-keys";
|
|
import { useAsyncData } from "./use-async-data";
|
|
|
|
export const useSSHKeys = () => {
|
|
const { data: keys, status, reload: loadKeys } = useAsyncData<SSHKey[]>(listSSHKeys, []);
|
|
const [newKeyName, setNewKeyName] = useState("");
|
|
const [generating, setGenerating] = useState(false);
|
|
const [signPayloads, setSignPayloads] = useState<Record<string, string>>({});
|
|
const [signatures, setSignatures] = useState<Record<string, string>>({});
|
|
const [signing, setSigning] = useState<Record<string, boolean>>({});
|
|
const [verifyPayloads, setVerifyPayloads] = useState<Record<string, string>>({});
|
|
const [verifySignatures, setVerifySignatures] = useState<Record<string, string>>({});
|
|
const [verifyResults, setVerifyResults] = useState<Record<string, boolean | null>>({});
|
|
const [verifying, setVerifying] = useState<Record<string, boolean>>({});
|
|
const [mutationError, setMutationError] = useState<string | null>(null);
|
|
|
|
const safeKeys = keys ?? [];
|
|
|
|
async function handleGenerate(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!newKeyName.trim()) return;
|
|
try {
|
|
setGenerating(true);
|
|
await createSSHKey({ name: newKeyName.trim() });
|
|
setNewKeyName("");
|
|
await loadKeys();
|
|
} catch {
|
|
setMutationError("Failed to generate SSH key");
|
|
} finally {
|
|
setGenerating(false);
|
|
}
|
|
}
|
|
|
|
async function handleDelete(keyId: string) {
|
|
if (!confirm("Are you sure you want to delete this SSH key?")) return;
|
|
try {
|
|
await deleteSSHKey(keyId);
|
|
await loadKeys();
|
|
} catch {
|
|
setMutationError("Failed to delete SSH key");
|
|
}
|
|
}
|
|
|
|
function copyToClipboard(text: string) {
|
|
navigator.clipboard.writeText(text);
|
|
}
|
|
|
|
async function handleSign(keyId: string) {
|
|
const payload = signPayloads[keyId];
|
|
if (!payload?.trim()) return;
|
|
try {
|
|
setSigning((prev) => ({ ...prev, [keyId]: true }));
|
|
const result = await signPayload(keyId, { payload: payload.trim() });
|
|
setSignatures((prev) => ({ ...prev, [keyId]: result.signature }));
|
|
setMutationError(null);
|
|
} catch {
|
|
setMutationError("Failed to sign payload");
|
|
} finally {
|
|
setSigning((prev) => ({ ...prev, [keyId]: false }));
|
|
}
|
|
}
|
|
|
|
async function handleVerify(keyId: string) {
|
|
const payload = verifyPayloads[keyId];
|
|
const signature = verifySignatures[keyId];
|
|
if (!payload?.trim() || !signature?.trim()) return;
|
|
try {
|
|
setVerifying((prev) => ({ ...prev, [keyId]: true }));
|
|
const result = await verifySignature(keyId, {
|
|
payload: payload.trim(),
|
|
signature: signature.trim(),
|
|
});
|
|
setVerifyResults((prev) => ({ ...prev, [keyId]: result.valid }));
|
|
setMutationError(null);
|
|
} catch {
|
|
setMutationError("Failed to verify signature");
|
|
} finally {
|
|
setVerifying((prev) => ({ ...prev, [keyId]: false }));
|
|
}
|
|
}
|
|
|
|
return {
|
|
keys: safeKeys,
|
|
status,
|
|
loadKeys,
|
|
newKeyName,
|
|
setNewKeyName,
|
|
generating,
|
|
mutationError,
|
|
setMutationError,
|
|
signPayloads,
|
|
setSignPayloads,
|
|
signatures,
|
|
signing,
|
|
verifyPayloads,
|
|
setVerifyPayloads,
|
|
verifySignatures,
|
|
setVerifySignatures,
|
|
verifyResults,
|
|
verifying,
|
|
handleGenerate,
|
|
handleDelete,
|
|
copyToClipboard,
|
|
handleSign,
|
|
handleVerify,
|
|
};
|
|
};
|