feat(hosts): Services, Lesemodelle und Aktionen fuer Hosts (#008)

Hosts und Hostgruppen brauchen eine Schreibschicht, bevor Host-Key-Pruefung
(#009) und Bootstrap (#010) darauf aufsetzen. Die Regeln (Name, Adresse wie
der DB-CHECK, Port, Tags, Anmeldeart) liegen framework-frei in src/lib/hosts.ts,
damit Action (Feldmeldungen) und Service (Schutz fuer Seed und Assistenten)
dieselben pruefen.

Archivieren statt Loeschen haelt Protokollbezuege lesbar; dabei fallen die
Gruppenmitgliedschaften weg, damit Zuweisungen an eine Gruppe nie einen
archivierten Host treffen. Jede Aenderung landet mit den geaenderten Feldern
im Protokoll, Eindeutigkeitsverletzungen kommen als deutsche HostFehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:42:56 +02:00
co-authored by Claude Opus 5.5
parent b194054d30
commit 4f9d5d9680
8 changed files with 1237 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import type { Benutzer } from "@/generated/prisma/client";
import { BestaetigungFehler, RechteFehler } from "@/server/auth/sitzungspflicht";
import { HostFehler, type Akteur } from "@/server/services/hosts";
/**
* Gemeinsames fuer Formular-Aktionen (#008). Kein "use server": diese Datei exportiert
* Hilfen, keine Aktionen – sonst waeren sie vom Browser aus aufrufbar.
*/
export type FormularZustand = {
readonly fehler?: string;
readonly feldfehler?: Record<string, string>;
};
/** Fachliche Fehler werden zur Meldung im Formular; alles andere bleibt ein echter Fehler. */
export function fehlerZustand(fehler: unknown): FormularZustand {
if (fehler instanceof HostFehler) {
return { fehler: fehler.message, feldfehler: fehler.feldfehler };
}
if (fehler instanceof RechteFehler || fehler instanceof BestaetigungFehler) {
return { fehler: fehler.message };
}
throw fehler;
}
export function akteurVon(benutzer: Pick<Benutzer, "id" | "name">): Akteur {
return { id: benutzer.id, name: benutzer.name };
}
/** Einzelnes Textfeld; fehlend bleibt `undefined`, damit Standardwerte des Schemas greifen. */
export function textfeld(formular: FormData, name: string): string | undefined {
const wert = formular.get(name);
return typeof wert === "string" && wert !== "" ? wert : undefined;
}
export function listenfeld(formular: FormData, name: string): string[] {
return formular.getAll(name).filter((w): w is string => typeof w === "string" && w !== "");
}
+68
View File
@@ -0,0 +1,68 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { feldfehlerAus, hostgruppeEingabeSchema, type HostgruppeEingabe } from "@/lib/hosts";
import {
akteurVon,
fehlerZustand,
listenfeld,
textfeld,
type FormularZustand,
} from "@/server/actions/formular";
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
import { hostgruppeAnlegen, hostgruppeBearbeiten } from "@/server/services/hostgruppen";
/** Formular-Aktionen fuer Hostgruppen (#008). Duenn wie die fuer Hosts. */
function gruppenFelder(formular: FormData): HostgruppeEingabe {
return {
name: textfeld(formular, "name") ?? "",
beschreibung: textfeld(formular, "beschreibung"),
hostIds: listenfeld(formular, "hostIds"),
};
}
function vorpruefen(eingabe: HostgruppeEingabe): FormularZustand | undefined {
const ergebnis = hostgruppeEingabeSchema.safeParse(eingabe);
if (ergebnis.success) return undefined;
return { fehler: "Bitte die markierten Felder prüfen.", feldfehler: feldfehlerAus(ergebnis.error) };
}
export async function hostgruppeAnlegenAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
let id: string;
try {
const { benutzer } = await rechtErforderlich("hostsVerwalten");
const eingabe = gruppenFelder(formular);
const ungueltig = vorpruefen(eingabe);
if (ungueltig) return ungueltig;
({ id } = await hostgruppeAnlegen(eingabe, akteurVon(benutzer)));
} catch (fehler) {
return fehlerZustand(fehler);
}
revalidatePath("/hostgruppen");
revalidatePath("/hosts", "layout");
redirect(`/hostgruppen/${id}`);
}
export async function hostgruppeBearbeitenAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
const id = textfeld(formular, "id") ?? "";
try {
const { benutzer } = await rechtErforderlich("hostsVerwalten");
const eingabe = gruppenFelder(formular);
const ungueltig = vorpruefen(eingabe);
if (ungueltig) return ungueltig;
await hostgruppeBearbeiten(id, eingabe, akteurVon(benutzer));
} catch (fehler) {
return fehlerZustand(fehler);
}
revalidatePath("/hostgruppen", "layout");
revalidatePath("/hosts", "layout");
redirect(`/hostgruppen/${id}`);
}
+93
View File
@@ -0,0 +1,93 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { feldfehlerAus, hostEingabeSchema, type HostEingabe } from "@/lib/hosts";
import {
akteurVon,
fehlerZustand,
listenfeld,
textfeld,
type FormularZustand,
} from "@/server/actions/formular";
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
import { hostAnlegen, hostArchivieren, hostBearbeiten } from "@/server/services/hosts";
/**
* Formular-Aktionen fuer Hosts (#008). Duenn: Recht pruefen, Eingabe pruefen, Service rufen,
* Seiten neu laden lassen, weiterleiten. `redirect` steht ausserhalb von try/catch, weil es
* per Ausnahme arbeitet.
*/
function hostFelder(formular: FormData): HostEingabe {
return {
name: textfeld(formular, "name") ?? "",
adresse: textfeld(formular, "adresse") ?? "",
port: textfeld(formular, "port"),
tags: textfeld(formular, "tags") ?? "",
beschreibung: textfeld(formular, "beschreibung"),
kundeName: textfeld(formular, "kundeName"),
anmeldeart: textfeld(formular, "anmeldeart") as HostEingabe["anmeldeart"],
gruppenIds: listenfeld(formular, "gruppenIds"),
};
}
function vorpruefen(eingabe: HostEingabe): FormularZustand | undefined {
const ergebnis = hostEingabeSchema.safeParse(eingabe);
if (ergebnis.success) return undefined;
return { fehler: "Bitte die markierten Felder prüfen.", feldfehler: feldfehlerAus(ergebnis.error) };
}
export async function hostAnlegenAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
let id: string;
try {
const { benutzer } = await rechtErforderlich("hostsVerwalten");
const eingabe = hostFelder(formular);
const ungueltig = vorpruefen(eingabe);
if (ungueltig) return ungueltig;
({ id } = await hostAnlegen(eingabe, akteurVon(benutzer)));
} catch (fehler) {
return fehlerZustand(fehler);
}
revalidatePath("/hosts");
revalidatePath("/hostgruppen", "layout");
redirect(`/hosts/${id}`);
}
export async function hostBearbeitenAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
const id = textfeld(formular, "id") ?? "";
try {
const { benutzer } = await rechtErforderlich("hostsVerwalten");
const eingabe = hostFelder(formular);
const ungueltig = vorpruefen(eingabe);
if (ungueltig) return ungueltig;
await hostBearbeiten(id, eingabe, akteurVon(benutzer));
} catch (fehler) {
return fehlerZustand(fehler);
}
revalidatePath("/hosts", "layout");
revalidatePath("/hostgruppen", "layout");
redirect(`/hosts/${id}`);
}
export async function hostArchivierenAktion(
_vorher: FormularZustand,
formular: FormData,
): Promise<FormularZustand> {
const id = textfeld(formular, "id") ?? "";
try {
const { benutzer } = await rechtErforderlich("hostsVerwalten");
await hostArchivieren(id, akteurVon(benutzer));
} catch (fehler) {
return fehlerZustand(fehler);
}
revalidatePath("/hosts", "layout");
revalidatePath("/hostgruppen", "layout");
redirect("/hosts");
}