diff --git a/drb-c2-core/app/routers/nodes.py b/drb-c2-core/app/routers/nodes.py index 238bf1b..9d443aa 100644 --- a/drb-c2-core/app/routers/nodes.py +++ b/drb-c2-core/app/routers/nodes.py @@ -36,6 +36,15 @@ async def approve_node(node_id: str, _: dict = Depends(require_admin_token)): return {"ok": True} +@router.delete("/{node_id}", status_code=204) +async def delete_node(node_id: str, _: dict = Depends(require_admin_token)): + node = await fstore.doc_get("nodes", node_id) + if not node: + raise HTTPException(404, f"Node '{node_id}' not found.") + await fstore.doc_delete("node_keys", node_id) + await fstore.doc_delete("nodes", node_id) + + @router.post("/{node_id}/reject") async def reject_node(node_id: str, _: dict = Depends(require_admin_token)): node = await fstore.doc_get("nodes", node_id) diff --git a/drb-frontend/app/nodes/[id]/page.tsx b/drb-frontend/app/nodes/[id]/page.tsx index 7ff3db2..320e098 100644 --- a/drb-frontend/app/nodes/[id]/page.tsx +++ b/drb-frontend/app/nodes/[id]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { doc, onSnapshot } from "firebase/firestore"; import { db } from "@/lib/firebase"; import { useSystems } from "@/lib/useSystems"; @@ -111,11 +111,13 @@ function DiscordJoinModal({ export default function NodeDetailPage() { const { id } = useParams<{ id: string }>(); + const router = useRouter(); const [node, setNode] = useState(null); const [showConfig, setShowConfig] = useState(false); const [showDiscordJoin, setShowDiscordJoin] = useState(false); const [sending, setSending] = useState(false); const [approving, setApproving] = useState(false); + const [deleting, setDeleting] = useState(false); const { systems } = useSystems(); const { calls } = useCalls(20); const { isAdmin } = useAuth(); @@ -150,6 +152,18 @@ export default function NodeDetailPage() { } } + async function handleDelete() { + if (!confirm(`Permanently delete node "${node?.name ?? id}"? This cannot be undone.`)) return; + setDeleting(true); + try { + await c2api.deleteNode(id); + router.push("/nodes"); + } catch (err) { + alert(err instanceof Error ? err.message : "Delete failed."); + setDeleting(false); + } + } + async function handleReject() { if (!confirm("Reject this node? It will not be able to upload recordings.")) return; setApproving(true); @@ -257,6 +271,15 @@ export default function NodeDetailPage() { > Leave Discord + {isAdmin && ( + + )} {/* Recent calls */} diff --git a/drb-frontend/lib/c2api.ts b/drb-frontend/lib/c2api.ts index dc87c93..8136c8f 100644 --- a/drb-frontend/lib/c2api.ts +++ b/drb-frontend/lib/c2api.ts @@ -52,6 +52,8 @@ export const c2api = { request(`/nodes/${id}/approve`, { method: "POST" }), rejectNode: (id: string) => request(`/nodes/${id}/reject`, { method: "POST" }), + deleteNode: (id: string) => + request(`/nodes/${id}`, { method: "DELETE" }), // Calls getCalls: (params?: Record) => {