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:
@@ -0,0 +1,137 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { akteurVon, fehlerZustand, textfeld, type FormularZustand } from "@/server/actions/formular";
|
||||
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import {
|
||||
erzeugenAnfordern,
|
||||
paarImportAnfordern,
|
||||
publicKeyImportieren,
|
||||
schluesselBearbeiten,
|
||||
} from "@/server/services/schluessel";
|
||||
|
||||
/**
|
||||
* Aktionen fuer Schluessel (#012). Duenn: Recht pruefen, Felder lesen, Service rufen.
|
||||
*
|
||||
* Passphrase und Private Key verlassen diese Funktionen nur verschluesselt im Job (der
|
||||
* Service verschluesselt sie) – nie in einer Antwort, einem Log oder dem Protokoll.
|
||||
*/
|
||||
export type SchluesselAntwort = FormularZustand & {
|
||||
/** Vorgang im Worker (Erzeugen, Paar-Import) – die Oberflaeche verfolgt ihn. */
|
||||
readonly vorgangId?: string;
|
||||
/** Sofort angelegt (Public-Key-Import). */
|
||||
readonly schluesselId?: string;
|
||||
};
|
||||
|
||||
function metadaten(formular: FormData) {
|
||||
return {
|
||||
name: textfeld(formular, "name") ?? "",
|
||||
slug: textfeld(formular, "slug"),
|
||||
kommentar: textfeld(formular, "kommentar"),
|
||||
tags: textfeld(formular, "tags") ?? "",
|
||||
ablaufAm: textfeld(formular, "ablaufAm"),
|
||||
personId: textfeld(formular, "personId"),
|
||||
vorlageId: textfeld(formular, "vorlageId"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Passphrase mit Wiederholung – die Wiederholung prueft der Server, nicht nur das Formular. */
|
||||
function passphraseLesen(formular: FormData): { passphrase?: string; fehler?: SchluesselAntwort } {
|
||||
const passphrase = textfeld(formular, "passphrase");
|
||||
const wiederholung = textfeld(formular, "passphraseWiederholung");
|
||||
if (passphrase && passphrase !== wiederholung) {
|
||||
return {
|
||||
fehler: {
|
||||
fehler: "Die Passphrasen stimmen nicht überein.",
|
||||
feldfehler: { passphraseWiederholung: "Stimmt nicht mit der Passphrase überein." },
|
||||
},
|
||||
};
|
||||
}
|
||||
return passphrase ? { passphrase } : {};
|
||||
}
|
||||
|
||||
export async function schluesselErzeugenAktion(
|
||||
_vorher: SchluesselAntwort,
|
||||
formular: FormData,
|
||||
): Promise<SchluesselAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
const { passphrase, fehler } = passphraseLesen(formular);
|
||||
if (fehler) return fehler;
|
||||
const vorgangId = await erzeugenAnfordern(
|
||||
{ ...metadaten(formular), typ: textfeld(formular, "typ") as "ED25519" | "RSA4096" | undefined },
|
||||
passphrase,
|
||||
akteurVon(benutzer),
|
||||
);
|
||||
return { vorgangId };
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function paarImportierenAktion(
|
||||
_vorher: SchluesselAntwort,
|
||||
formular: FormData,
|
||||
): Promise<SchluesselAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
const vorgangId = await paarImportAnfordern(
|
||||
metadaten(formular),
|
||||
textfeld(formular, "privateKey"),
|
||||
// Beim Import nur zur Pruefung – keine Wiederholung noetig, falsche merkt der Worker.
|
||||
textfeld(formular, "passphrase"),
|
||||
akteurVon(benutzer),
|
||||
);
|
||||
return { vorgangId };
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function publicKeyImportierenAktion(
|
||||
_vorher: SchluesselAntwort,
|
||||
formular: FormData,
|
||||
): Promise<SchluesselAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
const schluessel = await publicKeyImportieren(
|
||||
{
|
||||
...metadaten(formular),
|
||||
kategorie: textfeld(formular, "kategorie") as "PERSON" | "DIENST" | undefined,
|
||||
publicKey: textfeld(formular, "publicKey") ?? "",
|
||||
},
|
||||
akteurVon(benutzer),
|
||||
);
|
||||
revalidatePath("/schluessel");
|
||||
return { schluesselId: schluessel.id };
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function schluesselBearbeitenAktion(
|
||||
_vorher: FormularZustand,
|
||||
formular: FormData,
|
||||
): Promise<FormularZustand> {
|
||||
const id = textfeld(formular, "id") ?? "";
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("schluesselVerwalten");
|
||||
await schluesselBearbeiten(
|
||||
id,
|
||||
{
|
||||
name: textfeld(formular, "name") ?? "",
|
||||
tags: textfeld(formular, "tags") ?? "",
|
||||
ablaufAm: textfeld(formular, "ablaufAm"),
|
||||
personId: textfeld(formular, "personId"),
|
||||
vorlageId: textfeld(formular, "vorlageId"),
|
||||
status: textfeld(formular, "status") as "AKTIV" | "ABGELAUFEN" | undefined,
|
||||
},
|
||||
akteurVon(benutzer),
|
||||
);
|
||||
} catch (fehler) {
|
||||
return fehlerZustand(fehler);
|
||||
}
|
||||
revalidatePath("/schluessel", "layout");
|
||||
redirect(`/schluessel/${id}`);
|
||||
}
|
||||
Reference in New Issue
Block a user