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 { AusrollenPanel } from "@/components/ausrollen/AusrollenPanel"; import { EintragAktionen } from "@/components/ausrollen/EintragAktionen"; import { zielAuswahl } from "@/server/queries/ziele"; 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; aktionen?: ReactNode; }; function AusrollKarte({ titel, untertitel, eintraege, leer, aktion, }: { titel: string; untertitel: string; eintraege: Eintrag[]; leer: string; aktion: ReactNode; }) { return ( {eintraege.length === 0 ? (

{leer}

) : ( )}
); } function privateEintraege(s: SchluesselDetail, rechte: Rechte): 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, aktionen: ( ), }; }); } type Rechte = { readonly public: boolean; readonly privat: boolean; readonly testen: boolean }; function publicEintraege(s: SchluesselDetail, rechte: Rechte): 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, aktionen: ( ), })); } /** 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 ); } 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 rechte: Rechte = { public: darf(benutzer.rolle, "publicKeyAusrollen"), privat: darf(benutzer.rolle, "privateKeyAusrollen"), testen: darf(benutzer.rolle, "hostsVerwalten"), }; const auswahl = aktiv && rechte.public ? await zielAuswahl() : undefined; 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 && ( ) } />
{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 && ( } /> )} } />
{rechte.public && (s.publicVerwendungen.length > 0 || (rechte.privat && s.privateVerwendungen.length > 0)) && ( v.zustand !== "AUSGEROLLT").length + (rechte.privat ? s.privateVerwendungen.filter((v) => v.zustand !== "AUSGEROLLT").length : 0) } /> )} 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.

)}
); }