"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { api, Server } from "@/lib/api";
import { Badge, Button, Card, CardHeader, CardTitle } from "@/components/ui";
import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
function AssignModal({
keyId,
assignedServerIds,
onClose,
}: {
keyId: string;
assignedServerIds: string[];
onClose: () => void;
}) {
const queryClient = useQueryClient();
const [selectedServer, setSelectedServer] = useState("");
const { data: servers } = useQuery({
queryKey: ["servers"],
queryFn: api.listServers,
});
const { mutate: assign, isPending, error } = useMutation({
mutationFn: () => api.assignKey(keyId, selectedServer),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["keys", keyId] });
queryClient.invalidateQueries({ queryKey: ["servers"] });
onClose();
},
});
const availableServers = servers?.filter(
(s: Server) => !assignedServerIds.includes(s.server_id)
);
return (
Assign Key to Server
{error && (
{(error as Error).message}
)}
{!availableServers || availableServers.length === 0 ? (
All servers already have this key assigned.
) : (
)}
);
}
function PrivateKeyCard({ keyId }: { keyId: string }) {
const [revealed, setRevealed] = useState(false);
const [privateKey, setPrivateKey] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
async function reveal() {
setLoading(true);
setError(null);
try {
const res = await api.getPrivateKey(keyId);
setPrivateKey(res.private_key);
setRevealed(true);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
}
function download() {
if (!privateKey) return;
const blob = new Blob([privateKey], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${keyId}.pem`;
a.click();
URL.revokeObjectURL(url);
}
async function copy() {
if (!privateKey) return;
await navigator.clipboard.writeText(privateKey);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
Private Key
{revealed && (
)}
{error && (
{error}
)}
{!revealed ? (
Stored encrypted (AES-256-GCM). Click to decrypt and display.
) : (
)}
);
}
export default function KeyDetailPage() {
const params = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const keyId = params.id as string;
const [showAssign, setShowAssign] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [copiedKey, setCopiedKey] = useState(false);
const { data: key, isLoading, error } = useQuery({
queryKey: ["keys", keyId],
queryFn: () => api.getKey(keyId),
});
const { mutate: revokeKey } = useMutation({
mutationFn: (serverId: string) => api.revokeKey(keyId, serverId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["keys", keyId] });
queryClient.invalidateQueries({ queryKey: ["servers"] });
},
});
const { mutate: deleteKey, isPending: isDeleting } = useMutation({
mutationFn: () => api.deleteKey(keyId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["keys"] });
router.push("/keys");
},
});
const handleCopyKey = async () => {
if (!key?.public_key) return;
await navigator.clipboard.writeText(key.public_key);
setCopiedKey(true);
setTimeout(() => setCopiedKey(false), 2000);
};
if (isLoading) {
return (
);
}
if (error || !key) {
return (
Key not found or failed to load.
);
}
const activeAssignments = key.assignments?.filter((a) => !a.revoked_at) ?? [];
const assignedServerIds = activeAssignments.map((a) => a.server_id);
return (
{showAssign && (
setShowAssign(false)}
/>
)}
← SSH Keys
{key.label}
{key.source}
{key.fingerprint}
{!confirmDelete ? (
) : (
Delete permanently?
)}
Details
- Key ID
- {key.key_id}
- Source
- {key.source}
{key.generated_by_server_id && (
- Generated By
-
{key.generated_by_server_id}
)}
- Active Assignments
- {activeAssignments.length}
- Created
-
{new Date(key.created_at).toLocaleString()}
Public Key
{key.has_private_key &&
}
Server Assignments
{activeAssignments.length} active
{!key.assignments || key.assignments.length === 0 ? (
Not assigned to any servers.
) : (
| Server |
IP Address |
Status |
Assigned |
Revoked |
|
{key.assignments.map((assignment) => (
|
{assignment.server?.hostname ?? assignment.server_id}
|
{assignment.server?.ip_address ?? "—"}
|
{assignment.revoked_at ? "revoked" : "active"}
|
{new Date(assignment.assigned_at).toLocaleDateString()}
|
{assignment.revoked_at
? new Date(assignment.revoked_at).toLocaleDateString()
: "—"}
|
{!assignment.revoked_at && (
)}
|
))}
)}
);
}