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>
This commit is contained in:
2026-10-12 22:36:25 +02:00
co-authored by Claude Opus 5.5
parent d9eec3d991
commit 4219701b29
41 changed files with 1295 additions and 109 deletions
+47
View File
@@ -0,0 +1,47 @@
"use client";
import { UserPlus } from "lucide-react";
import { useActionState } from "react";
import { EinmalPasswort } from "@/components/benutzer/EinmalPasswort";
import { Auswahlliste, Button, Feld, Formularfehler } from "@/components/ui";
import { kontoAnlegenAktion, type KontoZustand } from "@/server/actions/benutzer";
/** Lokales Konto anlegen (#035). Das Passwort waehlt nicht der Admin, sshkv erzeugt es. */
export function KontoAnlegen() {
const [zustand, aktion, laeuft] = useActionState<KontoZustand, FormData>(kontoAnlegenAktion, {});
return (
<div className="flex flex-col gap-4">
{zustand.einmalPasswort && zustand.anmeldename && (
<EinmalPasswort anmeldename={zustand.anmeldename} passwort={zustand.einmalPasswort} />
)}
<form action={aktion} className="grid gap-4 sm:grid-cols-[1fr_1fr_12rem_auto] sm:items-end">
<div className="sm:col-span-4 empty:hidden">
<Formularfehler text={zustand.fehler} />
</div>
<Feld
beschriftung="Anmeldename"
name="anmeldename"
autoComplete="off"
required
mono
fehler={zustand.feldfehler?.["anmeldename"]}
/>
<Feld
beschriftung="Name"
name="name"
autoComplete="off"
required
fehler={zustand.feldfehler?.["name"]}
/>
<Auswahlliste beschriftung="Rolle" name="rolle" defaultValue="LESER">
<option value="LESER">Leser</option>
<option value="OPERATOR">Operator</option>
<option value="ADMIN">Admin</option>
</Auswahlliste>
<Button type="submit" variante="primaer" disabled={laeuft} icon={<UserPlus className="size-4" />}>
{laeuft ? "Lege an …" : "Konto anlegen"}
</Button>
</form>
</div>
);
}