import { useEffect, useState } from 'react'; import { api, SSHKey } from '../api/client'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Label } from '@/components/ui/Label'; import { Textarea } from '@/components/ui/Textarea'; import { Badge } from '@/components/ui/Badge'; import { Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, } from '@/components/ui/Modal'; import { PageHeader } from '@/components/ui/PageHeader'; import { Card, CardBody } from '@/components/ui/Card'; import { EmptyState } from '@/components/ui/EmptyState'; import { CopyButton } from '@/components/ui/CopyButton'; import { Spinner } from '@/components/ui/Spinner'; import { Key, Plus, Upload, Download, Trash2, Fingerprint, } from 'lucide-react'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; type ModalType = 'generate' | 'import' | 'delete' | null; export default function SSHKeys() { const [keys, setKeys] = useState([]); const [modal, setModal] = useState(null); const [deleteId, setDeleteId] = useState(null); const [genLabel, setGenLabel] = useState(''); const [importLabel, setImportLabel] = useState(''); const [importPubKey, setImportPubKey] = useState(''); const [loading, setLoading] = useState(false); const [downloading, setDownloading] = useState(null); useEffect(() => { load(); }, []); async function load() { try { setKeys(await api('/api/ssh-keys')); } catch {} } async function generate() { if (!genLabel.trim()) { toast.error('Label is required'); return; } setLoading(true); try { await api('/api/ssh-keys', { method: 'POST', body: { label: genLabel.trim(), generate: true }, }); setModal(null); setGenLabel(''); toast.success('SSH key pair generated'); load(); } catch (e: unknown) { toast.error((e as Error).message); } finally { setLoading(false); } } async function importKey() { if (!importLabel.trim()) { toast.error('Label is required'); return; } if (!importPubKey.trim()) { toast.error('Public key is required'); return; } setLoading(true); try { await api('/api/ssh-keys', { method: 'POST', body: { label: importLabel.trim(), generate: false, public_key: importPubKey.trim(), }, }); setModal(null); setImportLabel(''); setImportPubKey(''); toast.success('SSH key imported'); load(); } catch (e: unknown) { toast.error((e as Error).message); } finally { setLoading(false); } } async function handleDelete() { if (deleteId === null) return; try { await api(`/api/ssh-keys/${deleteId}`, { method: 'DELETE' }); toast.success('SSH key deleted'); setDeleteId(null); load(); } catch (e: unknown) { toast.error((e as Error).message); } } async function downloadPrivate(id: number) { try { const res = await fetch(`/api/ssh-keys/${id}/private`, { credentials: 'include', }); if (!res.ok) { const err = await res.json().catch(() => ({ error: 'Failed' })); toast.error((err as { error: string }).error); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ssh-key-${id}.key`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setDownloading(id); toast.success('Private key downloaded'); setTimeout(() => setDownloading(null), 3000); } catch (e: unknown) { toast.error((e as Error).message); } } return (
} /> {keys.length === 0 ? ( } title="No SSH keys" description="Generate a key pair or import a public key to authenticate with remote machines" action={
} />
) : (
{keys.map(k => (

{k.label}

{k.in_use && ( )} {!k.has_private_key && ( )}
{k.fingerprint}
{k.public_key}
{k.has_private_key && ( )}
))}
)} !v && setModal(null)}> Generate SSH Key Pair Generate a new Ed25519 key pair. The private key will be downloaded immediately and the public key stored on the server.
setGenLabel(e.target.value)} />
!v && setModal(null)}> Import Public Key Import an existing public key. Only the public key will be stored — you must have the corresponding private key on this server.
setImportLabel(e.target.value)} />