"use server"; import { revalidatePath } from "next/cache"; import { z } from "@/lib/validierung"; import { BestaetigungFehler, RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht"; import { akteurVon, textfeld, type FormularZustand } from "@/server/actions/formular"; import { darf } from "@/server/auth/rechte"; import { VerwendungsFehler, ausrollenStarten, privateVerwendungAnlegen, privateVerwendungEntfernen, publicVerwendungAnlegen, publicVerwendungEntfernen, trockenlaufStarten, } from "@/server/services/verwendungen"; import { instanzZiele } from "@/server/services/deploymentkey"; import { vorgangEinreihen } from "@/server/vorgaenge/vorgaenge"; /** Public-Key-Zuweisungen und Ausrollen (#016). Duenn: Recht, Eingabe, Service. */ export type VorgangsAntwort = { readonly vorgangId?: string; readonly fehler?: string }; function alsAntwort(fehler: unknown): { fehler: string } { if ( fehler instanceof VerwendungsFehler || fehler instanceof RechteFehler || fehler instanceof BestaetigungFehler ) { return { fehler: fehler.message }; } throw fehler; } export async function publicZuweisenAktion( _vorher: FormularZustand, formular: FormData, ): Promise { try { const { benutzer } = await rechtErforderlich("publicKeyAusrollen"); const schluesselId = z.string().min(1).max(64).parse(textfeld(formular, "schluesselId")); // „host:“ oder „gruppe:“ aus einer einzigen Auswahlliste. const ziel = textfeld(formular, "ziel") ?? ""; const [art, id] = ziel.split(":"); if (!id || (art !== "host" && art !== "gruppe")) return { feldfehler: { ziel: "Bitte ein Ziel wählen." } }; await publicVerwendungAnlegen( { schluesselId, hostId: art === "host" ? id : null, hostgruppeId: art === "gruppe" ? id : null, zielbenutzer: (textfeld(formular, "zielbenutzer") ?? "").trim(), optionen: textfeld(formular, "optionen") ?? null, }, akteurVon(benutzer), ); revalidatePath(`/schluessel/${schluesselId}`); return {}; } catch (fehler) { return alsAntwort(fehler); } } export async function publicEntfernenAktion( verwendungId: string, schluesselId: string, ): Promise<{ fehler?: string }> { try { const { benutzer } = await rechtErforderlich("publicKeyAusrollen"); await publicVerwendungEntfernen(z.string().min(1).max(64).parse(verwendungId), akteurVon(benutzer)); revalidatePath(`/schluessel/${schluesselId}`); return {}; } catch (fehler) { return alsAntwort(fehler); } } export type AusrollAuswahl = | { readonly schluesselId: string } | { readonly hostId: string } /** Ein gemeinsamer Dry-Run fuer mehrere Schluessel (Massenwiderruf, #024). */ | { readonly schluesselIds: readonly string[] } /** Alle Hosts mit dem Instanz-Deployment-Key – fuer dessen Rotation (#031). */ | { readonly deploymentKey: true }; export async function trockenlaufAktion(auswahl: AusrollAuswahl): Promise { try { if ("deploymentKey" in auswahl) { const { benutzer } = await rechtErforderlich("deploymentKeyVerwalten"); return { vorgangId: await trockenlaufStarten({ ziele: await instanzZiele() }, akteurVon(benutzer)) }; } const { benutzer } = await rechtErforderlich("publicKeyAusrollen"); const id = z.string().min(1).max(64); return { vorgangId: await trockenlaufStarten( "schluesselId" in auswahl ? { schluesselId: id.parse(auswahl.schluesselId) } : "schluesselIds" in auswahl ? { schluesselIds: z.array(id).min(1).max(200).parse(auswahl.schluesselIds) } : { hostId: id.parse(auswahl.hostId) }, akteurVon(benutzer), // Private-Key-Ziele nur fuer Admins – ein Operator rollt so nie einen Private Key aus. darf(benutzer.rolle, "privateKeyAusrollen"), ), }; } catch (fehler) { return alsAntwort(fehler); } } export async function ausrollenAktion(trockenlaufId: string): Promise { try { const { benutzer } = await rechtErforderlich("publicKeyAusrollen"); return { vorgangId: await ausrollenStarten(z.string().min(1).max(64).parse(trockenlaufId), akteurVon(benutzer)), }; } catch (fehler) { return alsAntwort(fehler); } } /** Private Key einem Quell-Host zuweisen (#017) – nur Admins. */ export async function privateZuweisenAktion( _vorher: FormularZustand, formular: FormData, ): Promise { try { const { benutzer } = await rechtErforderlich("privateKeyAusrollen"); const schluesselId = z.string().min(1).max(64).parse(textfeld(formular, "schluesselId")); const alias = (textfeld(formular, "alias") ?? "").trim(); const zielHostId = textfeld(formular, "zielHostId"); await privateVerwendungAnlegen( { schluesselId, hostId: textfeld(formular, "hostId") ?? "", zielbenutzer: (textfeld(formular, "zielbenutzer") ?? "").trim(), pfad: textfeld(formular, "pfad") ?? "", sshConfig: alias ? { alias, hostName: (textfeld(formular, "hostName") ?? "").trim(), benutzer: (textfeld(formular, "sshBenutzer") ?? "").trim(), port: Number(textfeld(formular, "port") ?? "22"), } : null, knownHostsFuer: zielHostId ? [zielHostId] : [], beimWiderruf: textfeld(formular, "beimWiderruf") === "UMBENENNEN" ? "UMBENENNEN" : "LOESCHEN", }, akteurVon(benutzer), ); revalidatePath(`/schluessel/${schluesselId}`); return {}; } catch (fehler) { return alsAntwort(fehler); } } export async function privateEntfernenAktion( verwendungId: string, schluesselId: string, ): Promise<{ fehler?: string }> { try { const { benutzer } = await rechtErforderlich("privateKeyAusrollen"); await privateVerwendungEntfernen(z.string().min(1).max(64).parse(verwendungId), akteurVon(benutzer)); revalidatePath(`/schluessel/${schluesselId}`); return {}; } catch (fehler) { return alsAntwort(fehler); } } /** Verbindungstest A → B (#020). Harmlos, aber nur fuer Rollen, die Hosts verwalten. */ export async function verbindungTestenAktion(privatVerwendungId: string): Promise { try { const { benutzer } = await rechtErforderlich("hostsVerwalten"); return { vorgangId: await vorgangEinreihen({ art: "VERBINDUNGSTEST", angefordertVonId: benutzer.id, eingabe: { privatVerwendungId: z.string().min(1).max(64).parse(privatVerwendungId) }, }), }; } catch (fehler) { return alsAntwort(fehler); } }