IAM-08: benutzerprofil-login-oberflaeche (login+2fa/passwort-reset/profil-backend-handler + web/account next.js-frontend auf shl-01)

This commit is contained in:
sysops
2026-08-28 23:34:36 +02:00
parent f0139c889a
commit f627caaacb
19 changed files with 1407 additions and 0 deletions
@@ -0,0 +1,71 @@
"use client";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { TextField } from "@nexarch/shl";
import { ApiError, completePasswordReset } from "../../../lib/api";
export default function PasswordResetCompletePage() {
const params = useSearchParams();
const token = params.get("token") ?? "";
const [newPassword, setNewPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);
const [submitting, setSubmitting] = useState(false);
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
setSubmitting(true);
try {
await completePasswordReset(token, newPassword);
setDone(true);
} catch (err) {
// Akzeptanzkriterium 3: generische Meldung, keine interne Fehlerursache.
setError(err instanceof ApiError ? err.message : "Der Link ist ungültig oder abgelaufen.");
} finally {
setSubmitting(false);
}
}
if (done) {
return (
<main style={{ maxWidth: 400, margin: "80px auto", padding: "0 16px" }}>
<h1>Passwort geändert</h1>
<p>
Ihr Passwort wurde erfolgreich geändert. <a href="/login">Jetzt anmelden</a>.
</p>
</main>
);
}
return (
<main style={{ maxWidth: 400, margin: "80px auto", padding: "0 16px" }}>
<h1>Neues Passwort festlegen</h1>
{!token && (
<p role="alert" style={{ color: "var(--shl-color-danger)" }}>
Kein gültiger Link. Bitte fordern Sie einen neuen Reset-Link an.
</p>
)}
<form onSubmit={onSubmit} noValidate>
<TextField
label="Neues Passwort"
type="password"
autoComplete="new-password"
required
minLength={8}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
{error && (
<p role="alert" style={{ color: "var(--shl-color-danger)" }}>
{error}
</p>
)}
<button type="submit" disabled={submitting || !token}>
{submitting ? "Wird gespeichert…" : "Passwort speichern"}
</button>
</form>
</main>
);
}
@@ -0,0 +1,53 @@
"use client";
import { useState } from "react";
import { TextField } from "@nexarch/shl";
import { requestPasswordReset } from "../../../lib/api";
export default function PasswordResetRequestPage() {
const [email, setEmail] = useState("");
const [message, setMessage] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setSubmitting(true);
try {
// Antwort ist absichtlich IMMER dieselbe (Backend-Garantie,
// Akzeptanzkriterium 3) — das Frontend erfindet keine differenzierte
// Anzeige je nachdem ob das Konto existiert.
const result = await requestPasswordReset(email);
setMessage(result.message);
} catch {
setMessage("Falls ein Konto mit dieser E-Mail-Adresse existiert, wurde eine Nachricht mit weiteren Schritten verschickt.");
} finally {
setSubmitting(false);
}
}
return (
<main style={{ maxWidth: 400, margin: "80px auto", padding: "0 16px" }}>
<h1>Passwort zurücksetzen</h1>
{message ? (
<p role="status">{message}</p>
) : (
<form onSubmit={onSubmit} noValidate>
<TextField
label="E-Mail-Adresse"
type="email"
autoComplete="username"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit" disabled={submitting}>
{submitting ? "Wird gesendet…" : "Link anfordern"}
</button>
</form>
)}
<p>
<a href="/login">Zurück zur Anmeldung</a>
</p>
</main>
);
}