Files
alpenwerk-hr/components/auth/PasswortAendernFormular.tsx
Andrei Laas 6903013548
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m12s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m6s
Link statt a-Element, und check deckt ab, was der CI prueft
2026-09-10 10:01:08 +02:00

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>
);
}