Files
sshkeyvault/src/server/actions/benutzer.ts
T
florian.wildeandClaude Opus 5.5 4219701b29 feat(anmeldung): lokale Benutzerkonten mit Pflicht-TOTP (#035)
Ohne authentik gab es nur den Notfall-Admin – ein geteiltes Konto ohne
Rollen und ohne erkennbare Akteure im Audit-Log. Admins legen jetzt
lokale Konten an; sshkv erzeugt ein Einmal-Passwort, das bei der ersten
Anmeldung ersetzt wird, danach ist TOTP Pflicht.

Rollen, Sperren (beendet Sitzungen sofort) und Zurücksetzen unter
Einstellungen → Benutzer, nur für Admins mit frischer Bestätigung.
SSHKV_LOKALE_KONTEN=0 schaltet lokale Konten ab; der Notfall-Admin
bleibt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:36:25 +02:00

83 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server";
import { revalidatePath } from "next/cache";
import { anmeldenameSchema, kontoNameSchema, kontoRolleSchema } from "@/lib/lokale-konten";
import { z } from "@/lib/validierung";
import { akteurVon, fehlerZustand, textfeld, type FormularZustand } from "@/server/actions/formular";
import { rechtErforderlich } from "@/server/auth/sitzungspflicht";
import {
kontoAktivSetzen,
kontoZuruecksetzen,
lokalesKontoAnlegen,
rolleAendern,
} from "@/server/services/benutzer";
/** Benutzerverwaltung (#035). Duenn: Recht mit Re-Auth, Eingabe, Service. */
const PFAD = "/einstellungen/benutzer";
const id = z.string().min(1).max(64);
/** Das Einmal-Passwort geht genau einmal an den Browser des Admins – nie ins Protokoll. */
export type KontoZustand = FormularZustand & {
readonly einmalPasswort?: string;
readonly anmeldename?: string;
};
export async function kontoAnlegenAktion(_vorher: KontoZustand, formular: FormData): Promise<KontoZustand> {
try {
const { benutzer } = await rechtErforderlich("benutzerVerwalten");
const eingabe = z
.object({ anmeldename: anmeldenameSchema, name: kontoNameSchema, rolle: kontoRolleSchema })
.safeParse({
anmeldename: textfeld(formular, "anmeldename") ?? "",
name: textfeld(formular, "name") ?? "",
rolle: textfeld(formular, "rolle") ?? "LESER",
});
if (!eingabe.success) {
const feldfehler: Record<string, string> = {};
for (const fehler of eingabe.error.issues) feldfehler[String(fehler.path[0])] ??= fehler.message;
return { feldfehler };
}
const { einmalPasswort } = await lokalesKontoAnlegen(eingabe.data, akteurVon(benutzer));
revalidatePath(PFAD);
return { einmalPasswort, anmeldename: eingabe.data.anmeldename };
} catch (fehler) {
return fehlerZustand(fehler);
}
}
export async function rolleAendernAktion(kontoId: string, rolle: string): Promise<{ fehler?: string }> {
try {
const { benutzer } = await rechtErforderlich("benutzerVerwalten");
await rolleAendern(id.parse(kontoId), kontoRolleSchema.parse(rolle), akteurVon(benutzer));
revalidatePath(PFAD);
return {};
} catch (fehler) {
return fehlerZustand(fehler);
}
}
export async function aktivSetzenAktion(kontoId: string, aktiv: boolean): Promise<{ fehler?: string }> {
try {
const { benutzer } = await rechtErforderlich("benutzerVerwalten");
await kontoAktivSetzen(id.parse(kontoId), z.boolean().parse(aktiv), akteurVon(benutzer));
revalidatePath(PFAD);
return {};
} catch (fehler) {
return fehlerZustand(fehler);
}
}
export async function zuruecksetzenAktion(
kontoId: string,
): Promise<{ fehler?: string; einmalPasswort?: string }> {
try {
const { benutzer } = await rechtErforderlich("benutzerVerwalten");
const { einmalPasswort } = await kontoZuruecksetzen(id.parse(kontoId), akteurVon(benutzer));
revalidatePath(PFAD);
return { einmalPasswort };
} catch (fehler) {
return fehlerZustand(fehler);
}
}