feat(server): Proxmox-Server verwalten mit Fingerprint-Pinning
Server lassen sich anlegen, prüfen, bearbeiten und löschen. Proxmox liefert ab Werk ein selbst signiertes Zertifikat; der Admin sieht es vor dem Speichern und bestätigt den SHA-256-Fingerprint. Der Pin wird in einem eigenen undici-Connector geprüft, sodass eine fremde Gegenstelle das Token nie zu sehen bekommt. Weder Prüfung noch Pin ist im Schema, im Client und per CHECK ausgeschlossen. Fehler von Proxmox werden zu Ursache und Hinweis; bei 403 nennt der Hinweis das fehlende Recht samt Pfad. Getestet gegen einen HTTPS-Mock und im gebauten Stand auf Desktop und 390 px. Closes #002 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { Pencil } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Code } from "@/components/formular";
|
||||
import { Karte } from "@/components/karte";
|
||||
import { Status } from "@/components/status";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import { seiteSchuetzen } from "@/server/auth/zugang";
|
||||
import { serverSicht } from "@/server/services/server";
|
||||
import { ServerStatus } from "../server-status";
|
||||
import { ServerAktionen } from "./server-aktionen";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ name: string }> }): Promise<Metadata> {
|
||||
return { title: (await params).name };
|
||||
}
|
||||
|
||||
export default async function ServerDetail({ params }: { params: Promise<{ name: string }> }) {
|
||||
const { name } = await params;
|
||||
const benutzer = await seiteSchuetzen("lesen", `/server/${name}`);
|
||||
const datensatz = await prisma.server.findUnique({ where: { name } });
|
||||
if (!datensatz) notFound();
|
||||
const server = serverSicht(datensatz);
|
||||
const verwalten = darf(benutzer.rolle, "serverVerwalten");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<header className="flex flex-wrap items-end justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-muted font-mono text-sm">{server.name}</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">{server.titel}</h1>
|
||||
</div>
|
||||
{verwalten ? (
|
||||
<Link
|
||||
href={`/server/${server.name}/bearbeiten`}
|
||||
className="border-linie bg-panel-hoch hover:border-muted inline-flex h-11 items-center gap-2 rounded-lg border px-4 text-sm font-semibold sm:h-10"
|
||||
>
|
||||
<Pencil size={16} aria-hidden="true" /> Bearbeiten
|
||||
</Link>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<Karte titel="Verbindung" aktion={<ServerStatus server={server} />}>
|
||||
<dl className="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-[max-content_1fr]">
|
||||
<dt className="text-muted">Adresse</dt>
|
||||
<dd className="font-mono break-all">{server.url}</dd>
|
||||
<dt className="text-muted">Token-ID</dt>
|
||||
<dd className="font-mono break-all">{server.tokenId}</dd>
|
||||
<dt className="text-muted">TLS</dt>
|
||||
<dd>
|
||||
{server.fingerabdruck ? (
|
||||
<span className="flex flex-col gap-1">
|
||||
<span>Fingerprint gepinnt{server.tlsPruefen ? " und Kette geprüft" : ""}</span>
|
||||
<span className="text-muted font-mono text-xs break-all">{server.fingerabdruck}</span>
|
||||
</span>
|
||||
) : (
|
||||
"Kette gegen Systemzertifikate geprüft"
|
||||
)}
|
||||
</dd>
|
||||
<dt className="text-muted">Templates</dt>
|
||||
<dd className="font-mono">
|
||||
{server.templateVmidVon}–{server.templateVmidBis}
|
||||
</dd>
|
||||
<dt className="text-muted">Gäste ab</dt>
|
||||
<dd className="font-mono">{server.gastVmidVon}</dd>
|
||||
</dl>
|
||||
</Karte>
|
||||
|
||||
<Karte titel="SSH zu den Nodes (optional)">
|
||||
{!server.sshAktiv ? (
|
||||
<Status art="offen">Nicht genutzt. Deployments laufen dann nur über die API.</Status>
|
||||
) : server.sshOeffentlich ? (
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<p>
|
||||
Diesen Schlüssel auf jedem Node für <span className="font-mono">{server.sshBenutzer}</span>{" "}
|
||||
hinterlegen. Im Cluster genügt ein Node:{" "}
|
||||
<span className="font-mono">/root/.ssh/authorized_keys</span> zeigt dort auf die gemeinsame
|
||||
Datei in <span className="font-mono">/etc/pve/priv/</span>.
|
||||
</p>
|
||||
<Code>{`echo '${server.sshOeffentlich}' >> ~/.ssh/authorized_keys`}</Code>
|
||||
<p className="text-muted">
|
||||
Host-Keys werden beim ersten Verbindungstest angezeigt und nach Bestätigung gepinnt (#003).
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Status art="laeuft">SSH ist eingeschaltet, aber es gibt noch keinen Schlüssel.</Status>
|
||||
)}
|
||||
{verwalten && server.sshAktiv ? (
|
||||
<div className="mt-4">
|
||||
<ServerAktionen name={server.name} art="ssh" erneuern={Boolean(server.sshOeffentlich)} />
|
||||
</div>
|
||||
) : null}
|
||||
</Karte>
|
||||
|
||||
{verwalten ? (
|
||||
<Karte titel="Gefahrenzone">
|
||||
<ServerAktionen name={server.name} art="loeschen" />
|
||||
</Karte>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user