feat(schluessel): erzeugen, importieren, exportieren (#012)

Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste
mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem
Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub.

Warum so:
- Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil
  nur er den Master-Key hat. Passphrase und importierter Private Key
  reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt
  im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach.
- Ein reiner Public Key ist kein Geheimnis und wird direkt im Web
  angelegt, mit verstaendlicher Meldung bei einem Duplikat.
- Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist,
  verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein
  struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf
  Schluessel ausserhalb des Workers ein festes select.
- Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das
  Loeschen des Private Keys; CHECKs fuer den Slug (landet in
  Dateinamen) und "geloescht nur ohne Chiffrat".
- Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 10:29:52 +02:00
co-authored by Claude Opus 5.5
parent f0833aa1db
commit 263143134f
33 changed files with 3322 additions and 287 deletions
+89
View File
@@ -124,10 +124,99 @@ async function demoDaten(): Promise<void> {
console.log(`Demo-Daten geprüft: ${DEMO_HOSTS.length} Hosts, ${DEMO_GRUPPEN.length} Gruppen.`);
}
/**
* Demo-Personen und -Schluessel (#012, #013). Nur Public Keys: einen Private Key kann nur
* der Worker versiegeln. Die Paare werden hier frisch erzeugt und der Private Key sofort
* verworfen – es sind Attrappen, die nirgends Zugang geben.
*/
const DEMO_PERSONEN = [
{ name: "Florian Wilde", email: "florian@example.org", organisation: "IT" },
{ name: "Anna Berger", email: "a.berger@netzwerk-gmbh.example", organisation: "Netzwerk GmbH" },
{ name: "Mehmet Yılmaz", email: "mehmet@example.org", organisation: "IT" },
] as const;
const TAG_MS = 24 * 60 * 60 * 1000;
const DEMO_SCHLUESSEL: {
name: string;
slug: string;
kategorie: "PERSON" | "DIENST";
person?: string;
tags: string[];
ablaufInTagen?: number;
kommentar: string;
}[] = [
{
name: "Laptop Florian",
slug: "laptop-florian",
kategorie: "PERSON",
person: "Florian Wilde",
tags: ["laptop"],
kommentar: "florian@laptop",
},
{
name: "Wartungszugang Netzwerk GmbH",
slug: "wartung-netzwerk-gmbh",
kategorie: "PERSON",
person: "Anna Berger",
tags: ["dienstleister"],
ablaufInTagen: 18,
kommentar: "anna@netzwerk-gmbh",
},
{
name: "Workstation Mehmet",
slug: "workstation-mehmet",
kategorie: "PERSON",
person: "Mehmet Yılmaz",
tags: [],
ablaufInTagen: 200,
kommentar: "mehmet@ws-07",
},
{
name: "Monitoring zabbix → alle Hosts",
slug: "monitoring-zabbix",
kategorie: "DIENST",
tags: ["monitoring"],
kommentar: "zabbix@srv-mon01",
},
];
async function demoSchluessel(): Promise<void> {
if (process.env["SSHKV_SEED_DEMO"] !== "1") return;
const { schluesselpaarErzeugen } = await import("../src/lib/ssh/openssh");
const personen = new Map<string, string>();
for (const p of DEMO_PERSONEN) {
const vorhanden = await prisma.person.findFirst({ where: { name: p.name }, select: { id: true } });
const person = vorhanden ?? (await prisma.person.create({ data: p, select: { id: true } }));
personen.set(p.name, person.id);
}
for (const s of DEMO_SCHLUESSEL) {
if (await prisma.schluessel.findUnique({ where: { slug: s.slug }, select: { id: true } })) continue;
const paar = await schluesselpaarErzeugen("ED25519", { kommentar: s.kommentar });
await prisma.schluessel.create({
data: {
name: s.name,
slug: s.slug,
typ: "ED25519",
kategorie: s.kategorie,
publicKey: paar.publicKeyZeile,
fingerprint: paar.fingerprint,
kommentar: s.kommentar,
tags: s.tags,
personId: s.person ? (personen.get(s.person) ?? null) : null,
ablaufAm: s.ablaufInTagen ? new Date(Date.now() + s.ablaufInTagen * TAG_MS) : null,
},
select: { id: true },
});
}
console.log(`Demo-Daten geprüft: ${DEMO_PERSONEN.length} Personen, ${DEMO_SCHLUESSEL.length} Schlüssel.`);
}
try {
await notfallAdmin();
await vorlagen();
await demoDaten();
await demoSchluessel();
} finally {
await prisma.$disconnect();
}