| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- "use client";
- import React from "react";
- import { useAuth } from "@/components/auth/authContext";
- import { changePassword, ApiClientError } from "@/lib/frontend/apiClient";
- import {
- notifyError,
- notifySuccess,
- notifyApiError,
- } from "@/lib/frontend/ui/toast";
- import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
- import {
- getPasswordPolicyHintLinesDe,
- reasonsToHintLinesDe,
- buildWeakPasswordMessageDe,
- } from "@/lib/frontend/profile/passwordPolicyUi";
- import { Button } from "@/components/ui/button";
- import { Input } from "@/components/ui/input";
- import { Label } from "@/components/ui/label";
- import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
- import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
- CardFooter,
- } from "@/components/ui/card";
- function isNonEmptyString(value) {
- return typeof value === "string" && value.trim().length > 0;
- }
- export default function ChangePasswordCard() {
- const { status, user } = useAuth();
- const isAuthenticated = status === "authenticated" && user;
- const [currentPassword, setCurrentPassword] = React.useState("");
- const [newPassword, setNewPassword] = React.useState("");
- const [confirmNewPassword, setConfirmNewPassword] = React.useState("");
- const [isSubmitting, setIsSubmitting] = React.useState(false);
- const [error, setError] = React.useState(null);
- // error: { title: string, description?: string|null, field?: string|null, hints?: string[]|null }
- const policyLines = React.useMemo(() => {
- return getPasswordPolicyHintLinesDe();
- }, []);
- function clearForm() {
- setCurrentPassword("");
- setNewPassword("");
- setConfirmNewPassword("");
- }
- function redirectToLoginExpired() {
- const next =
- typeof window !== "undefined"
- ? `${window.location.pathname}${window.location.search}`
- : "/profile";
- window.location.replace(
- buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
- );
- }
- async function onSubmit(e) {
- e.preventDefault();
- if (isSubmitting) return;
- setError(null);
- if (!isAuthenticated) {
- notifyError({
- title: "Nicht angemeldet",
- description: "Bitte melden Sie sich an, um Ihr Passwort zu ändern.",
- });
- return;
- }
- if (!isNonEmptyString(currentPassword)) {
- setError({
- field: "currentPassword",
- title: "Bitte aktuelles Passwort eingeben.",
- description: null,
- });
- return;
- }
- if (!isNonEmptyString(newPassword)) {
- setError({
- field: "newPassword",
- title: "Bitte ein neues Passwort eingeben.",
- description: null,
- });
- return;
- }
- if (!isNonEmptyString(confirmNewPassword)) {
- setError({
- field: "confirmNewPassword",
- title: "Bitte neues Passwort bestätigen.",
- description: null,
- });
- return;
- }
- if (newPassword !== confirmNewPassword) {
- setError({
- field: "confirmNewPassword",
- title: "Passwörter stimmen nicht überein.",
- description: "Bitte prüfen Sie die Bestätigung.",
- });
- return;
- }
- if (newPassword === currentPassword) {
- setError({
- field: "newPassword",
- title: "Neues Passwort ist ungültig.",
- description:
- "Neues Passwort darf nicht identisch zum aktuellen Passwort sein.",
- });
- return;
- }
- setIsSubmitting(true);
- try {
- await changePassword({
- currentPassword,
- newPassword,
- });
- clearForm();
- notifySuccess({
- title: "Passwort geändert",
- description: "Ihr Passwort wurde erfolgreich aktualisiert.",
- });
- } catch (err) {
- if (err instanceof ApiClientError) {
- if (err.code === "AUTH_UNAUTHENTICATED") {
- notifyApiError(err);
- redirectToLoginExpired();
- return;
- }
- if (err.code === "AUTH_INVALID_CREDENTIALS") {
- const title = "Aktuelles Passwort ist falsch.";
- setError({ field: "currentPassword", title, description: null });
- notifyError({ title });
- return;
- }
- if (err.code === "VALIDATION_WEAK_PASSWORD") {
- const reasons = err.details?.reasons;
- const minLength = err.details?.minLength;
- const hints = reasonsToHintLinesDe({ reasons, minLength });
- const description = buildWeakPasswordMessageDe({
- reasons,
- minLength,
- });
- setError({
- field: "newPassword",
- title: "Neues Passwort ist zu schwach.",
- description,
- hints,
- });
- notifyError({
- title: "Neues Passwort ist zu schwach.",
- description,
- });
- return;
- }
- }
- setError({
- field: null,
- title: "Passwort konnte nicht geändert werden.",
- description: "Bitte versuchen Sie es erneut.",
- });
- notifyApiError(err, {
- fallbackTitle: "Passwort konnte nicht geändert werden.",
- fallbackDescription: "Bitte versuchen Sie es erneut.",
- });
- } finally {
- setIsSubmitting(false);
- }
- }
- const showError = Boolean(error && error.title);
- return (
- <Card>
- <CardHeader>
- <CardTitle>Passwort</CardTitle>
- <CardDescription>Ändern Sie Ihr Passwort.</CardDescription>
- </CardHeader>
- <CardContent className="space-y-4">
- {!isAuthenticated ? (
- <p className="text-sm text-muted-foreground">
- Hinweis: Passwortänderungen sind nur verfügbar, wenn Sie angemeldet
- sind.
- </p>
- ) : null}
- {showError ? (
- <Alert variant="destructive">
- <AlertTitle>{error.title}</AlertTitle>
- {error.description ? (
- <AlertDescription>{error.description}</AlertDescription>
- ) : null}
- {Array.isArray(error.hints) && error.hints.length > 0 ? (
- <AlertDescription>
- <ul className="mt-2 list-disc pl-5">
- {error.hints.map((line) => (
- <li key={line}>{line}</li>
- ))}
- </ul>
- </AlertDescription>
- ) : null}
- </Alert>
- ) : null}
- <form onSubmit={onSubmit} className="space-y-4">
- <div className="grid gap-2">
- <Label htmlFor="currentPassword">Aktuelles Passwort</Label>
- <Input
- id="currentPassword"
- type="password"
- autoComplete="current-password"
- value={currentPassword}
- onChange={(e) => setCurrentPassword(e.target.value)}
- disabled={!isAuthenticated || isSubmitting}
- aria-invalid={
- error?.field === "currentPassword" ? "true" : "false"
- }
- />
- </div>
- <div className="grid gap-2">
- <Label htmlFor="newPassword">Neues Passwort</Label>
- <Input
- id="newPassword"
- type="password"
- autoComplete="new-password"
- value={newPassword}
- onChange={(e) => setNewPassword(e.target.value)}
- disabled={!isAuthenticated || isSubmitting}
- aria-invalid={error?.field === "newPassword" ? "true" : "false"}
- />
- <ul className="mt-1 list-disc pl-5 text-xs text-muted-foreground">
- {policyLines.map((line) => (
- <li key={line}>{line}</li>
- ))}
- </ul>
- </div>
- <div className="grid gap-2">
- <Label htmlFor="confirmNewPassword">
- Neues Passwort bestätigen
- </Label>
- <Input
- id="confirmNewPassword"
- type="password"
- autoComplete="new-password"
- value={confirmNewPassword}
- onChange={(e) => setConfirmNewPassword(e.target.value)}
- disabled={!isAuthenticated || isSubmitting}
- aria-invalid={
- error?.field === "confirmNewPassword" ? "true" : "false"
- }
- />
- </div>
- <CardFooter className="p-0 flex justify-end">
- <Button
- type="submit"
- disabled={!isAuthenticated || isSubmitting}
- title={!isAuthenticated ? "Bitte anmelden" : "Passwort ändern"}
- >
- {isSubmitting ? "Speichern…" : "Passwort ändern"}
- </Button>
- </CardFooter>
- </form>
- </CardContent>
- </Card>
- );
- }
|