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>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
"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>
|
|
);
|
|
}
|