"use client"; import { ShieldCheck } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { EINGABE_KLASSE, Feld, Schalter, beschreibung, Code } from "@/components/formular"; import { Karte } from "@/components/karte"; import { Knopf } from "@/components/knopf"; import { Status } from "@/components/status"; import { apiAufruf, ApiFehler } from "@/lib/api-client"; import type { ServerSicht } from "@/lib/schemas/server"; type Zertifikat = { fingerabdruck: string; subjekt: string; aussteller: string; gueltigBis: string; alternativeNamen: string[]; vertraut: boolean; fehlerGrund: string | null; }; type PruefErgebnis = { zertifikat: Zertifikat | null; pinPasst: boolean | null; verbindung: | { ok: true; version: string; release: string } | { ok: false; art: string; ursache: string; hinweis: string } | null; }; type Werte = { name: string; titel: string; url: string; tokenId: string; tokenGeheimnis: string; tlsPruefen: boolean; fingerabdruck: string | null; sshAktiv: boolean; sshBenutzer: string; sshPort: string; templateVmidVon: string; templateVmidBis: string; gastVmidVon: string; }; function anfangswerte(server?: ServerSicht): Werte { return { name: server?.name ?? "", titel: server?.titel ?? "", url: server?.url ?? "", tokenId: server?.tokenId ?? "", tokenGeheimnis: "", tlsPruefen: server?.tlsPruefen ?? false, fingerabdruck: server?.fingerabdruck ?? null, sshAktiv: server?.sshAktiv ?? false, sshBenutzer: server?.sshBenutzer ?? "root", sshPort: String(server?.sshPort ?? 22), templateVmidVon: String(server?.templateVmidVon ?? 9000), templateVmidBis: String(server?.templateVmidBis ?? 9999), gastVmidVon: String(server?.gastVmidVon ?? 100), }; } /** * Anlegen und Bearbeiten eines Proxmox-Servers. Ablauf: Adresse → Zertifikat ansehen und * bestätigen → Token → Verbindung prüfen → speichern. Speichern geht auch ohne erfolgreiche * Prüfung (der Host kann gerade aus sein), aber nie ohne bestätigten Fingerprint oder TLS-Prüfung. */ export function ServerFormular({ server }: { server?: ServerSicht }) { const router = useRouter(); const bearbeiten = Boolean(server); const [werte, setWerte] = useState(() => anfangswerte(server)); const [pruefung, setPruefung] = useState(null); const [prueft, setPrueft] = useState<"zertifikat" | "verbindung" | null>(null); const [speichert, setSpeichert] = useState(false); const [felder, setFelder] = useState>({}); const [meldung, setMeldung] = useState<{ text: string; hinweis?: string } | null>(null); const setze = (feld: K, wert: Werte[K]) => { setWerte((alt) => ({ ...alt, [feld]: wert })); // Eine neue Adresse macht das gesehene Zertifikat und den Pin wertlos. if (feld === "url") { setPruefung(null); setWerte((alt) => ({ ...alt, fingerabdruck: null })); } }; async function pruefen(was: "zertifikat" | "verbindung") { setPrueft(was); setMeldung(null); try { const ergebnis = await apiAufruf("POST", "/api/v1/server/pruefen", { url: werte.url, name: server?.name, tokenId: was === "verbindung" ? werte.tokenId : undefined, tokenGeheimnis: was === "verbindung" ? werte.tokenGeheimnis : undefined, tlsPruefen: werte.tlsPruefen, fingerabdruck: werte.fingerabdruck, }); setPruefung(ergebnis); // Ein vertrauenswürdiges Zertifikat (ACME) braucht keinen Pin – TLS-Prüfung genügt. if (was === "zertifikat" && ergebnis.zertifikat?.vertraut && !werte.fingerabdruck) { setWerte((alt) => ({ ...alt, tlsPruefen: true })); } } catch (fehler) { setMeldung(fehlerText(fehler)); if (fehler instanceof ApiFehler) setFelder(fehler.felder); } finally { setPrueft(null); } } async function speichern(ereignis: React.FormEvent) { ereignis.preventDefault(); setSpeichert(true); setFelder({}); setMeldung(null); const rumpf = { ...werte, sshPort: Number(werte.sshPort), templateVmidVon: Number(werte.templateVmidVon), templateVmidBis: Number(werte.templateVmidBis), gastVmidVon: Number(werte.gastVmidVon), }; try { const gespeichert = bearbeiten ? await apiAufruf("PATCH", `/api/v1/server/${server!.name}`, rumpf) : await apiAufruf("POST", "/api/v1/server", rumpf); router.push(`/server/${gespeichert.name}`); router.refresh(); } catch (fehler) { setMeldung(fehlerText(fehler)); if (fehler instanceof ApiFehler) setFelder(fehler.felder); } finally { setSpeichert(false); } } const zertifikat = pruefung?.zertifikat; const gepinnt = Boolean(werte.fingerabdruck); return (
setze("name", e.target.value)} autoComplete="off" spellCheck={false} {...beschreibung("name", felder["name"], true)} /> setze("titel", e.target.value)} placeholder="Rechenzentrum 1" {...beschreibung("titel", felder["titel"])} />
setze("url", e.target.value)} placeholder="https://pve01:8006" inputMode="url" spellCheck={false} {...beschreibung("url", felder["url"], true)} /> pruefen("zertifikat")} disabled={!werte.url || prueft !== null} className="sm:mb-[1.625rem]" > {prueft === "zertifikat" ? "Holt …" : "Zertifikat holen"}
{zertifikat ? (
{zertifikat.vertraut ? "Das System vertraut diesem Zertifikat." : "Selbst signiertes Zertifikat – bitte den Fingerprint vergleichen."}
Subjekt
{zertifikat.subjekt}
Aussteller
{zertifikat.aussteller}
Gültig bis
{new Date(zertifikat.gueltigBis).toLocaleDateString("de-DE")}
SHA-256-Fingerprint {zertifikat.fingerabdruck}

Zum Vergleichen auf dem Node: pvenode cert info oder in der Proxmox-Oberfläche unter Node → System → Zertifikate.

setWerte((alt) => ({ ...alt, fingerabdruck: an ? zertifikat.fingerabdruck : null })) } />
) : gepinnt ? (
{werte.fingerabdruck!}
) : null} {felder["fingerabdruck"] ? (

{felder["fingerabdruck"]}

) : null} setze("tlsPruefen", an)} />
setze("tokenId", e.target.value)} placeholder="proxdeploy@pve!deploy" autoComplete="off" spellCheck={false} {...beschreibung("tokenId", felder["tokenId"], true)} /> setze("tokenGeheimnis", e.target.value)} placeholder={bearbeiten ? "unverändert" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"} autoComplete="new-password" {...beschreibung("tokenGeheimnis", felder["tokenGeheimnis"], bearbeiten)} />

Welche Rechte das Token braucht, steht im README unter „Proxmox-Rechte“.

pruefen("verbindung")} disabled={ prueft !== null || !werte.url || !werte.tokenId || (!werte.tokenGeheimnis && !bearbeiten) || (!werte.tlsPruefen && !gepinnt) } > {prueft === "verbindung" ? "Prüft …" : "Verbindung prüfen"}
{pruefung?.verbindung?.ok ? ( Verbunden · Proxmox VE {pruefung.verbindung.version} ) : pruefung?.verbindung && !pruefung.verbindung.ok ? (
{pruefung.verbindung.ursache}

{pruefung.verbindung.hinweis}

) : null}
setze("templateVmidVon", e.target.value)} /> setze("templateVmidBis", e.target.value)} /> setze("gastVmidVon", e.target.value)} />
setze("sshAktiv", an)} /> {werte.sshAktiv ? (
setze("sshBenutzer", e.target.value)} {...beschreibung("sshBenutzer", undefined, true)} /> setze("sshPort", e.target.value)} />
) : null}
{meldung ? (
{meldung.text} {meldung.hinweis ?

{meldung.hinweis}

: null}
) : null}
router.back()}> Abbrechen {speichert ? "Speichert …" : bearbeiten ? "Änderungen speichern" : "Server anlegen"}
); } function fehlerText(fehler: unknown): { text: string; hinweis?: string } { if (fehler instanceof ApiFehler) return { text: fehler.message, hinweis: fehler.hinweis }; return { text: "Unerwarteter Fehler." }; }