106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useActionState, useState } from "react";
|
|
import { passwortAendern, type WechselZustand } from "@/actions/passwort";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { CONTROL_CLASS, Field } from "@/components/ui/Field";
|
|
import { PASSWORT_REGELN, passwortBeanstandung } from "@/lib/passwort";
|
|
|
|
// Das eigene Passwort wechseln.
|
|
//
|
|
// Die Regeln stehen schon beim Tippen da, statt erst nach dem Absenden — beim
|
|
// erzwungenen Wechsel ist das der erste Bildschirm, den jemand in dieser
|
|
// Anwendung sieht, und dreimal abgewiesen zu werden ist ein schlechter Anfang.
|
|
// Abgewiesen oder angenommen wird trotzdem in der Datenbank; hier steht nur
|
|
// eine Vorabanzeige (lib/passwort.ts).
|
|
|
|
const START: WechselZustand = { fehler: null };
|
|
|
|
export function PasswortAendernFormular({ erzwungen }: { erzwungen: boolean }) {
|
|
const [zustand, aktion, laeuft] = useActionState(passwortAendern, START);
|
|
const [neues, setNeues] = useState("");
|
|
const [wiederholung, setWiederholung] = useState("");
|
|
|
|
// Erst beanstanden, wenn etwas dasteht: ein leeres Feld ist noch kein Fehler.
|
|
const beanstandung = neues ? passwortBeanstandung(neues) : null;
|
|
const passtNichtZusammen = wiederholung.length > 0 && neues !== wiederholung;
|
|
|
|
return (
|
|
<form action={aktion} className="flex flex-col gap-4">
|
|
{zustand.fehler && (
|
|
<p role="alert" className="rounded-md border border-danger-text/20 bg-danger-bg px-3 py-2 text-sm text-danger-text">
|
|
{zustand.fehler}
|
|
</p>
|
|
)}
|
|
|
|
<Field label="Bisheriges Passwort">
|
|
{(p) => (
|
|
<input {...p} name="altes_passwort" type="password" required autoComplete="current-password" className={CONTROL_CLASS} />
|
|
)}
|
|
</Field>
|
|
|
|
<Field
|
|
label="Neues Passwort"
|
|
error={beanstandung}
|
|
hint={`Erforderlich: ${PASSWORT_REGELN.join(", ")}.`}
|
|
>
|
|
{(p) => (
|
|
<input
|
|
{...p}
|
|
name="neues_passwort"
|
|
type="password"
|
|
required
|
|
autoComplete="new-password"
|
|
value={neues}
|
|
onChange={(e) => setNeues(e.target.value)}
|
|
className={CONTROL_CLASS}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
<Field label="Neues Passwort wiederholen" error={passtNichtZusammen ? "Die beiden Eingaben stimmen nicht überein." : null}>
|
|
{(p) => (
|
|
<input
|
|
{...p}
|
|
name="wiederholung"
|
|
type="password"
|
|
required
|
|
autoComplete="new-password"
|
|
value={wiederholung}
|
|
onChange={(e) => setWiederholung(e.target.value)}
|
|
className={CONTROL_CLASS}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
{/* Gesperrt, solange die Vorabanzeige etwas zu beanstanden hat. Das ist
|
|
Bequemlichkeit, keine Absicherung — wer die Schaltfläche freischaltet,
|
|
landet in app_passwort_regeln() und wird dort abgewiesen. */}
|
|
<Button
|
|
type="submit"
|
|
fullWidth
|
|
disabled={Boolean(beanstandung) || passtNichtZusammen}
|
|
pending={laeuft}
|
|
pendingLabel="Wird gespeichert…"
|
|
>
|
|
Passwort ändern
|
|
</Button>
|
|
|
|
{/* `Link` und nicht `<a>`: ein gewöhnlicher Verweis auf eine Seite dieser
|
|
Anwendung lädt das Dokument neu und damit das ganze Bündel — der Weg
|
|
zurück fühlte sich an wie ein Neustart. eslint-config-next weist das
|
|
ab, und zu Recht. */}
|
|
{!erzwungen && (
|
|
<Link
|
|
href="/"
|
|
className="rounded text-center text-xs text-ink-muted underline underline-offset-2 hover:no-underline
|
|
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
|
|
>
|
|
Zurück zur Anwendung
|
|
</Link>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|