import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh_keys"; import { EmptyState, ErrorState, LoadingState } from "../components/data-states"; import { Icon } from "../components/icon"; import { useAsyncData } from "../hooks/use-async-data"; export const SSHKeysPage = () => { const navigate = useNavigate(); const { data: keys, status, error, reload: loadKeys } = useAsyncData(listSSHKeys, []); const [newKeyName, setNewKeyName] = useState(""); const [generating, setGenerating] = useState(false); const [signPayloads, setSignPayloads] = useState>({}); const [signatures, setSignatures] = useState>({}); const [signing, setSigning] = useState>({}); const [verifyPayloads, setVerifyPayloads] = useState>({}); const [verifySignatures, setVerifySignatures] = useState>({}); const [verifyResults, setVerifyResults] = useState>({}); const [verifying, setVerifying] = useState>({}); const [mutationError, setMutationError] = useState(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 })); } } if (status === "loading") return ; return (

Settings

SSH Keys

{mutationError &&
{mutationError}
}
setNewKeyName(e.target.value)} placeholder="e.g., GitHub Work" required />
{status === "error" && }
{safeKeys.length === 0 ? ( ) : ( safeKeys.map((key) => (

{key.name}

Created: {new Date(key.created_at).toLocaleDateString()}
{key.public_key.substring(0, 50)}...

Sign Payload