import { Download, Lock, LockOpen, Pencil } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import type { ReactNode } from "react";
import { AblaufBadge, KategorieBadge, StatusBadge, TypBadge } from "@/components/schluessel/Anzeige";
import { Badge } from "@/components/ui/Badge";
import { Breadcrumb } from "@/components/ui/Breadcrumb";
import { Button } from "@/components/ui/Button";
import { InnereBox, Karte } from "@/components/ui/Karte";
import { MonoBlock } from "@/components/ui/MonoBlock";
import { MonoKopieren } from "@/components/ui/MonoKopieren";
import { Seitenkopf } from "@/components/ui/Seitenkopf";
import { Statuspunkt, type HostStatus } from "@/components/ui/Statuspunkt";
import { datum, datumZeit } from "@/lib/format";
import { darf } from "@/server/auth/rechte";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
import { schluesselDetail, type SchluesselDetail } from "@/server/queries/schluessel";
import { PrivateAusrollenAktion } from "./PrivateAusrollenAktion";
import { PrivateKeyAnzeigen, PrivateKeyLoeschen } from "./PrivateKeyAktionen";
import { PublicAusrollenAktion } from "./PublicAusrollenAktion";
export const metadata: Metadata = { title: "Schlüssel" };
function InfoFeld({ titel, children }: { titel: string; children: ReactNode }) {
return (
{titel}
{children}
);
}
/** Soll-Zustand einer Verwendung als Statuspunkt – Form und Text tragen die Information mit. */
const ZUSTAND: Record = {
GEPLANT: { status: "unbekannt", text: "geplant" },
AUSGEROLLT: { status: "synchron", text: "ausgerollt" },
ABWEICHUNG: { status: "drift", text: "Abweichung" },
ENTFERNEN: { status: "unbekannt", text: "wird entfernt" },
FEHLGESCHLAGEN: { status: "drift", text: "fehlgeschlagen" },
};
type Eintrag = { id: string; ziel: string; href?: string; pfad: string; zusatz?: string; zustand: string };
function AusrollKarte({
titel,
untertitel,
eintraege,
leer,
aktion,
}: {
titel: string;
untertitel: string;
eintraege: Eintrag[];
leer: string;
aktion: ReactNode;
}) {
return (
{eintraege.length === 0 ? (
{leer}
) : (
{eintraege.map((e) => {
const z = ZUSTAND[e.zustand] ?? { status: "unbekannt" as const, text: e.zustand };
return (
{e.href ? (
{e.ziel}
) : (
e.ziel
)}
{e.pfad}
{e.zusatz &&
{e.zusatz}
}
);
})}
)}
);
}
function privateEintraege(s: SchluesselDetail): Eintrag[] {
return s.privateVerwendungen.map((v) => {
const config =
v.sshConfig && typeof v.sshConfig === "object" ? (v.sshConfig as { alias?: unknown }) : null;
return {
id: v.id,
ziel: `${v.zielbenutzer}@${v.host.name}`,
href: `/hosts/${v.host.id}`,
pfad: v.pfad,
...(typeof config?.alias === "string" ? { zusatz: `config: Host ${config.alias}` } : {}),
zustand: v.zustand,
};
});
}
function publicEintraege(s: SchluesselDetail): Eintrag[] {
return s.publicVerwendungen.map((v) => ({
id: v.id,
ziel: v.host
? `${v.zielbenutzer}@${v.host.name}`
: `${v.zielbenutzer}@${v.hostgruppe?.name ?? "?"} (Gruppe)`,
...(v.host
? { href: `/hosts/${v.host.id}` }
: v.hostgruppe
? { href: `/hostgruppen/${v.hostgruppe.id}` }
: {}),
pfad: "~/.ssh/authorized_keys",
...(v.optionen ? { zusatz: v.optionen } : {}),
zustand: v.zustand,
}));
}
/** Was mit dem Private Key ist – ohne ihn je zu lesen (masterKeyVersion verraet es). */
function PrivatStatus({ s, anzeigen }: { s: SchluesselDetail; anzeigen?: ReactNode }) {
if (s.privatGespeichert) {
return (
verschlüsselt gespeichert
{s.passphraseGeschuetzt && · mit Passphrase }
{anzeigen}
);
}
if (s.privatGeloeschtAm) {
return (
gelöscht am {datum(s.privatGeloeschtAm)}
);
}
return (
nicht gespeichert{s.kategorie === "PERSON" ? " (Personen-Key)" : ""}
);
}
/** Schlüsseldetail (#012) nach dem Prototyp aus #002; Ausrollen folgt mit #016/#017. */
export default async function SchluesselSeite({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const { benutzer } = await sitzungErforderlich("lesen", `/schluessel/${id}`);
const s = await schluesselDetail(id);
if (!s) notFound();
const aktiv = s.status === "AKTIV";
const personenKey = s.kategorie === "PERSON";
const verwalten = darf(benutzer.rolle, "schluesselVerwalten");
// Den Deployment-Key gibt sshkv nie heraus (#014) – Knoepfe dafuer erscheinen gar nicht erst.
const privatHerausgebbar = s.privatGespeichert && s.kategorie !== "DEPLOYMENT";
const darfAnzeigen = privatHerausgebbar && darf(benutzer.rolle, "privateKeyAnzeigen");
const darfLoeschen = privatHerausgebbar && darf(benutzer.rolle, "privateKeyLoeschen");
const jetzt = new Date();
return (
}
titel={s.name}
untertitel={
s.person ? (
<>
Person:{" "}
{s.person.name}
>
) : undefined
}
badges={
<>
{s.instanzStandard &&
Instanz-Standard }
{s.selbstHinterlegt &&
selbst hinterlegt }
{s.vorlage &&
Vorlage: {s.vorlage.name} }
{s.tags.map((t) => (
#{t}
))}
>
}
aktionen={
verwalten && (
}>
Bearbeiten
)
}
/>
{datumZeit(s.angelegtAm)}
{s.erstelltVon ? `von ${s.erstelltVon.name}` : "von sshkv"}
·
>
)
}
/>
{/* Personen-Keys haben nie einen gespeicherten Private Key (DB-CHECK) – die Karte dafuer
waere immer leer. */}
{!personenKey && (
}
/>
)}
}
/>
Kommentar: {s.kommentar || "–"}
>
}
aktionen={
{s.slug}.pub
}
>
{darfLoeschen && (
Sinnvoll, sobald der Private Key ausgerollt ist und sshkv ihn nicht mehr braucht. Danach kann
ihn niemand mehr aus SSHKeyVault holen – auch kein Admin.
)}
);
}