feat(benutzer): Benutzerverwaltung, Passwort ändern, Audit-Log-Ansicht
Admins verwalten Benutzer (anlegen, Rolle, sperren, Passwort setzen); jeder ändert sein Passwort; das Audit-Log ist filterbar einsehbar. Sperre und Passwortwechsel beenden ältere Sitzungen über Benutzer.sitzungenAb. Der letzte Admin und das eigene Konto sind geschützt. Closes #015 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { EINGABE_KLASSE, Feld } from "@/components/formular";
|
||||
import { Karte } from "@/components/karte";
|
||||
import { Knopf } from "@/components/knopf";
|
||||
import { Status } from "@/components/status";
|
||||
import { apiAufruf, ApiFehler } from "@/lib/api-client";
|
||||
|
||||
/** Eigenes Passwort ändern. Andere Sitzungen (andere Geräte) enden, diese bleibt angemeldet. */
|
||||
export function PasswortAendern() {
|
||||
const [werte, setWerte] = useState({ alt: "", neu: "", wiederholt: "" });
|
||||
const [meldung, setMeldung] = useState<{
|
||||
art: "erledigt" | "fehler";
|
||||
text: string;
|
||||
felder?: Record<string, string>;
|
||||
} | null>(null);
|
||||
const abweichend = werte.wiederholt !== "" && werte.neu !== werte.wiederholt;
|
||||
|
||||
async function speichern(ereignis: React.FormEvent) {
|
||||
ereignis.preventDefault();
|
||||
try {
|
||||
await apiAufruf("POST", "/api/v1/ich/passwort", { alt: werte.alt, neu: werte.neu });
|
||||
setWerte({ alt: "", neu: "", wiederholt: "" });
|
||||
setMeldung({ art: "erledigt", text: "Passwort geändert. Andere Sitzungen sind beendet." });
|
||||
} catch (fehler) {
|
||||
setMeldung({
|
||||
art: "fehler",
|
||||
text: fehler instanceof ApiFehler ? fehler.message : "Fehler.",
|
||||
felder: fehler instanceof ApiFehler ? fehler.felder : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Karte titel="Passwort">
|
||||
<form onSubmit={(e) => void speichern(e)} className="flex flex-col gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Feld id="pw-alt" label="Bisheriges Passwort" fehler={meldung?.felder?.["alt"]}>
|
||||
<input
|
||||
id="pw-alt"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.alt}
|
||||
onChange={(e) => setWerte({ ...werte, alt: e.target.value })}
|
||||
/>
|
||||
</Feld>
|
||||
<Feld
|
||||
id="pw-neu"
|
||||
label="Neues Passwort"
|
||||
hilfe="Mindestens 12 Zeichen"
|
||||
fehler={meldung?.felder?.["neu"]}
|
||||
>
|
||||
<input
|
||||
id="pw-neu"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.neu}
|
||||
onChange={(e) => setWerte({ ...werte, neu: e.target.value })}
|
||||
/>
|
||||
</Feld>
|
||||
<Feld
|
||||
id="pw-wieder"
|
||||
label="Neues Passwort wiederholen"
|
||||
fehler={abweichend ? "Stimmt nicht überein." : undefined}
|
||||
>
|
||||
<input
|
||||
id="pw-wieder"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.wiederholt}
|
||||
onChange={(e) => setWerte({ ...werte, wiederholt: e.target.value })}
|
||||
/>
|
||||
</Feld>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Knopf
|
||||
type="submit"
|
||||
disabled={!werte.alt || werte.neu.length < 12 || werte.neu !== werte.wiederholt}
|
||||
>
|
||||
Passwort ändern
|
||||
</Knopf>
|
||||
<div aria-live="polite" className="text-sm">
|
||||
{meldung ? <Status art={meldung.art}>{meldung.text}</Status> : null}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Karte>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user