Merge branch 'phase/019-vorlagen'

This commit is contained in:
2026-10-12 10:54:31 +02:00
4 changed files with 418 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { VorlagenFehler, type VorlageEingabe } from "@/lib/vorlagen";
import { akteurVon, fehlerZustand, textfeld, type FormularZustand } from "@/server/actions/formular";
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
import { vorlageAnlegen, vorlageArchivieren, vorlageBearbeiten } from "@/server/services/vorlagen";
/** Vorlagen-Aktionen (#019): nur Admins (Recht einstellungenVerwalten). */
function felder(formular: FormData): VorlageEingabe {
const tage = textfeld(formular, "standardLaufzeitTage");
return {
name: textfeld(formular, "name") ?? "",
beschreibung: textfeld(formular, "beschreibung") ?? null,
schluesselTyp: textfeld(formular, "schluesselTyp") === "RSA4096" ? "RSA4096" : "ED25519",
kategorie: textfeld(formular, "kategorie") === "PERSON" ? "PERSON" : "DIENST",
standardLaufzeitTage: tage ? Number(tage) : null,
zielbenutzer: (textfeld(formular, "zielbenutzer") ?? "").trim(),
zielbenutzerAnlegen: formular.get("zielbenutzerAnlegen") === "ja",
sudoModus:
(["KEINER", "BEFEHLE", "VOLL"] as const).find((m) => m === textfeld(formular, "sudoModus")) ?? "KEINER",
sudoBefehle: (textfeld(formular, "sudoBefehle") ?? "")
.split("\n")
.map((b) => b.trim())
.filter(Boolean),
authorizedKeysOptionen: textfeld(formular, "authorizedKeysOptionen") ?? null,
sshConfigAlias: textfeld(formular, "sshConfigAlias") ?? null,
knownHostsEintragen: formular.get("knownHostsEintragen") === "ja",
privatNachAusrollenLoeschen: formular.get("privatNachAusrollenLoeschen") === "ja",
};
}
function alsZustand(fehler: unknown): FormularZustand {
if (fehler instanceof VorlagenFehler) {
return {
fehler: fehler.message,
...(fehler.feld ? { feldfehler: { [fehler.feld]: fehler.message } } : {}),
};
}
return fehlerZustand(fehler);
}
export async function vorlageSpeichernAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
const id = textfeld(formular, "id");
try {
const { benutzer } = await rechtErforderlich("einstellungenVerwalten");
if (id) await vorlageBearbeiten(id, felder(formular), akteurVon(benutzer));
else await vorlageAnlegen(felder(formular), akteurVon(benutzer));
} catch (fehler) {
return alsZustand(fehler);
}
revalidatePath("/vorlagen");
redirect("/vorlagen");
}
export async function vorlageArchivierenAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
try {
const { benutzer } = await rechtErforderlich("einstellungenVerwalten");
await vorlageArchivieren(textfeld(formular, "id") ?? "", akteurVon(benutzer));
} catch (fehler) {
return alsZustand(fehler);
}
revalidatePath("/vorlagen");
redirect("/vorlagen");
}
+74
View File
@@ -0,0 +1,74 @@
import { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/db";
import { vorlagePruefen, VorlagenFehler, type VorlageEingabe } from "@/lib/vorlagen";
import { protokollieren } from "@/server/services/protokoll";
/** Vorlagen anlegen, bearbeiten, archivieren (#019). Eingebaute lassen sich nur bearbeiten. */
type Akteur = { readonly id: string; readonly name: string };
function daten(eingabe: VorlageEingabe) {
const v = vorlagePruefen(eingabe);
return {
name: v.name.trim(),
beschreibung: v.beschreibung?.trim() || null,
schluesselTyp: v.schluesselTyp,
kategorie: v.kategorie,
standardLaufzeitTage: v.standardLaufzeitTage ?? null,
zielbenutzer: v.zielbenutzer,
zielbenutzerAnlegen: v.zielbenutzerAnlegen,
sudoModus: v.sudoModus,
sudoBefehle: v.sudoModus === "BEFEHLE" ? v.sudoBefehle.map((b) => b.trim().replace(/\s+/g, " ")) : [],
authorizedKeysOptionen: v.authorizedKeysOptionen?.trim() || null,
sshConfigAlias: v.sshConfigAlias?.trim() || null,
knownHostsEintragen: v.knownHostsEintragen,
privatNachAusrollenLoeschen: v.privatNachAusrollenLoeschen,
};
}
function eindeutig(fehler: unknown): never {
if (fehler instanceof Prisma.PrismaClientKnownRequestError && fehler.code === "P2002") {
throw new VorlagenFehler("Eine Vorlage mit diesem Namen gibt es bereits.", "name");
}
throw fehler;
}
export async function vorlageAnlegen(eingabe: VorlageEingabe, akteur: Akteur): Promise<string> {
const werte = daten(eingabe);
const vorlage = await prisma.vorlage.create({ data: werte }).catch(eindeutig);
await protokollieren({
akteur,
aktion: "vorlage.angelegt",
ergebnis: "ERFOLG",
details: { name: werte.name },
});
return vorlage.id;
}
export async function vorlageBearbeiten(id: string, eingabe: VorlageEingabe, akteur: Akteur): Promise<void> {
const werte = daten(eingabe);
const alt = await prisma.vorlage.findUnique({ where: { id } });
if (!alt || alt.archiviertAm) throw new VorlagenFehler("Diese Vorlage gibt es nicht (mehr).");
await prisma.vorlage.update({ where: { id }, data: werte }).catch(eindeutig);
const geaendert = Object.entries(werte)
.filter(([feld, wert]) => JSON.stringify(alt[feld as keyof typeof alt]) !== JSON.stringify(wert))
.map(([feld]) => feld);
await protokollieren({
akteur,
aktion: "vorlage.geaendert",
ergebnis: "ERFOLG",
details: { name: werte.name, geaendert },
});
}
export async function vorlageArchivieren(id: string, akteur: Akteur): Promise<void> {
const vorlage = await prisma.vorlage.findUnique({ where: { id } });
if (!vorlage || vorlage.archiviertAm) throw new VorlagenFehler("Diese Vorlage gibt es nicht (mehr).");
if (vorlage.eingebaut) throw new VorlagenFehler("Eingebaute Vorlagen lassen sich nicht archivieren.");
await prisma.vorlage.update({ where: { id }, data: { archiviertAm: new Date() } });
await protokollieren({
akteur,
aktion: "vorlage.archiviert",
ergebnis: "ERFOLG",
details: { name: vorlage.name },
});
}