Zwei berechtigte Rueckmeldungen von Florian: 1. In der Benutzerverwaltung standen die Passwortfelder auf type='text', das Passwort war also im Klartext lesbar - vor fremden Augen und in jeder Bildschirmuebertragung. Alle Passwortfelder nutzen jetzt ein gemeinsames PasswordInput: verdeckt als Voreinstellung, mit Auge zum Aufdecken. Weil ein Administrator das Passwort weitergeben muss, gibt es daneben einen Generator fuer ein zufaelliges, vorlesbares Passwort in Vierergruppen. 2. Es gab keinen Kontobereich. Neu ist /konto mit Profil, den eigenen Gruppen samt Rollenerklaerung, Passwortwechsel und Abmelden. Im Kopfbereich fuehrt ein Namenskuerzel dorthin. Ist die Anmeldung ausgeschaltet, steht dort 'Ohne Anmeldung' und die Seite erklaert, wie man sie einschaltet - vorher suchte man den Abmelden-Knopf vergeblich. Dabei eine echte Luecke geschlossen: ein Benutzer konnte sein Passwort gar nicht selbst aendern, nur ein Administrator konnte es setzen. Der Wechsel verlangt das bisherige Passwort, lehnt ein identisches ab und beendet alle anderen Sitzungen - die eigene bleibt bestehen. Nebenbei: die Betriebsart-Knoepfe im Generator haben jetzt data-mode. Ueber aria-pressed waren sie nicht mehr eindeutig, seit das Passwortfeld einen eigenen Umschalter hat. Geprueft: 63 Pruefungen fuer Anmeldung inkl. Passwortwechsel und Konto, dazu 280 bestehende bei ausgeschalteter Anmeldung.
195 lines
5.4 KiB
TypeScript
195 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
|
|
import { PasswordInput } from "@/components/password-input";
|
|
import { Button, Card, Field, Notice, TextInput } from "@/components/ui";
|
|
import { ApiError, apiJson } from "@/lib/api-client";
|
|
|
|
function useSubmit() {
|
|
const router = useRouter();
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const submit = async (json: unknown) => {
|
|
setPending(true);
|
|
setError(null);
|
|
try {
|
|
await apiJson("/api/auth", { method: "POST", json });
|
|
// refresh() ist hier entscheidend: Kopfbereich und Startseite werden
|
|
// serverseitig je nach Anmeldung unterschiedlich gerendert, und ohne
|
|
// Verwerfen des Zwischenspeichers käme die Seite von vor der Anmeldung.
|
|
router.replace("/");
|
|
router.refresh();
|
|
} catch (caught) {
|
|
setError(
|
|
caught instanceof ApiError
|
|
? caught.message
|
|
: "Die Anmeldung ist fehlgeschlagen.",
|
|
);
|
|
setPending(false);
|
|
}
|
|
};
|
|
|
|
return { submit, pending, error, router };
|
|
}
|
|
|
|
export function LoginForm() {
|
|
const { submit, pending, error } = useSubmit();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
|
|
return (
|
|
<Card>
|
|
<form
|
|
className="flex flex-col gap-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void submit({ action: "login", email, password });
|
|
}}
|
|
>
|
|
{error ? <Notice tone="error">{error}</Notice> : null}
|
|
|
|
<Field label="E-Mail-Adresse" htmlFor="login-email">
|
|
<TextInput
|
|
id="login-email"
|
|
type="email"
|
|
autoComplete="username"
|
|
autoFocus
|
|
required
|
|
value={email}
|
|
disabled={pending}
|
|
onChange={(event) => setEmail(event.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field label="Passwort" htmlFor="login-password">
|
|
<PasswordInput
|
|
id="login-password"
|
|
autoComplete="current-password"
|
|
required
|
|
value={password}
|
|
disabled={pending}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Button type="submit" disabled={pending}>
|
|
{pending ? "Wird angemeldet …" : "Anmelden"}
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function SetupForm({ minLength }: { minLength: number }) {
|
|
const { submit, pending, error } = useSubmit();
|
|
const [email, setEmail] = useState("");
|
|
const [name, setName] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [repeat, setRepeat] = useState("");
|
|
|
|
const mismatch = repeat.length > 0 && password !== repeat;
|
|
|
|
return (
|
|
<Card>
|
|
<form
|
|
className="flex flex-col gap-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (mismatch) return;
|
|
void submit({ action: "setup", email, name, password });
|
|
}}
|
|
>
|
|
{error ? <Notice tone="error">{error}</Notice> : null}
|
|
|
|
<Field label="Name (optional)" htmlFor="setup-name">
|
|
<TextInput
|
|
id="setup-name"
|
|
autoComplete="name"
|
|
value={name}
|
|
disabled={pending}
|
|
onChange={(event) => setName(event.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field label="E-Mail-Adresse" htmlFor="setup-email">
|
|
<TextInput
|
|
id="setup-email"
|
|
type="email"
|
|
autoComplete="username"
|
|
required
|
|
value={email}
|
|
disabled={pending}
|
|
onChange={(event) => setEmail(event.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field
|
|
label="Passwort"
|
|
htmlFor="setup-password"
|
|
hint={`Mindestens ${minLength} Zeichen.`}
|
|
>
|
|
<PasswordInput
|
|
id="setup-password"
|
|
autoComplete="new-password"
|
|
required
|
|
minLength={minLength}
|
|
value={password}
|
|
disabled={pending}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field
|
|
label="Passwort wiederholen"
|
|
htmlFor="setup-repeat"
|
|
error={mismatch ? "Die beiden Passwörter stimmen nicht überein." : null}
|
|
>
|
|
<PasswordInput
|
|
id="setup-repeat"
|
|
autoComplete="new-password"
|
|
required
|
|
invalid={mismatch}
|
|
value={repeat}
|
|
disabled={pending}
|
|
onChange={(event) => setRepeat(event.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Button type="submit" disabled={pending || mismatch}>
|
|
{pending ? "Wird angelegt …" : "Administrator anlegen"}
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function LogoutButton() {
|
|
const router = useRouter();
|
|
const [pending, setPending] = useState(false);
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
disabled={pending}
|
|
onClick={async () => {
|
|
setPending(true);
|
|
try {
|
|
await apiJson("/api/auth", {
|
|
method: "POST",
|
|
json: { action: "logout" },
|
|
});
|
|
} finally {
|
|
router.replace("/anmelden");
|
|
router.refresh();
|
|
}
|
|
}}
|
|
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 disabled:opacity-50"
|
|
>
|
|
{pending ? "…" : "Abmelden"}
|
|
</button>
|
|
);
|
|
}
|