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>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
"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);
|
||
}
|
||
}
|