Passwort-Anmeldung: Provider, Formulare, drei Zustaende der Shell
This commit is contained in:
100
components/auth/PasswortAendernFormular.tsx
Normal file
100
components/auth/PasswortAendernFormular.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
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>
|
||||
|
||||
{!erzwungen && (
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
54
components/auth/PasswortFormular.tsx
Normal file
54
components/auth/PasswortFormular.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { anmeldenMitPasswort, type AnmeldeZustand } from "@/actions/auth";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { CONTROL_CLASS, Field } from "@/components/ui/Field";
|
||||
|
||||
// Anmeldung mit Passwort — der zweite Weg neben dem Firmenkonto, auf Zeit
|
||||
// (siehe actions/auth.ts).
|
||||
//
|
||||
// Die Meldung kommt aus der Server Action und ist für jeden Fehlschlag
|
||||
// dieselbe. Kein `autoFocus`: die Seite bietet zwei Wege an, und der obere ist
|
||||
// der gemeinte — den Cursor unten hineinzusetzen kehrte das um.
|
||||
|
||||
const START: AnmeldeZustand = { fehler: null };
|
||||
|
||||
export function PasswortFormular() {
|
||||
const [zustand, aktion, laeuft] = useActionState(anmeldenMitPasswort, START);
|
||||
|
||||
return (
|
||||
<form action={aktion} className="flex flex-col gap-3">
|
||||
{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="E-Mail-Adresse">
|
||||
{(p) => (
|
||||
<input
|
||||
{...p}
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
// `username`, nicht `email`: nur so bietet der Passwortspeicher des
|
||||
// Browsers das Paar aus Adresse und Passwort gemeinsam an.
|
||||
autoComplete="username"
|
||||
className={CONTROL_CLASS}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field label="Passwort">
|
||||
{(p) => (
|
||||
<input {...p} name="passwort" type="password" required autoComplete="current-password" className={CONTROL_CLASS} />
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Button type="submit" fullWidth pending={laeuft} pendingLabel="Anmelden…">
|
||||
Anmelden
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user