import React, { useCallback, useEffect, useMemo, useState } from "react"; import axios from "axios"; import { Card, CardBody, CardHeader, Typography, Input, Button, Spinner, Alert } from "@material-tailwind/react"; import { useNavigate } from "react-router-dom"; import { useToast } from "@/components/ToastProvider.jsx"; import { useAuth } from "@/components/AuthProvider.jsx"; const PHRASE_WORD_COUNT = 8; const normalizeWord = (value) => String(value || "").trim().toLowerCase(); const extractWordsFromText = (text = "") => { const normalized = String(text || "").replace(/[\r\n\t]+/g, " ").trim().toLowerCase(); if (!normalized) { return []; } return normalized.split(/\s+/).filter(Boolean); }; const buildPhrasePayload = (words = []) => { const normalized = words.map(normalizeWord).filter(Boolean); return { words: normalized, canonical: normalized.join("") }; }; export function ForgotPassword() { const navigate = useNavigate(); const { showToast } = useToast(); const { user } = useAuth(); const [username, setUsername] = useState(""); const [phraseInputs, setPhraseInputs] = useState(Array(PHRASE_WORD_COUNT).fill("")); const [fileName, setFileName] = useState(""); const [fileContent, setFileContent] = useState(""); const [verificationError, setVerificationError] = useState(""); const [verifying, setVerifying] = useState(false); const [verificationToken, setVerificationToken] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [resetting, setResetting] = useState(false); const [resetError, setResetError] = useState(""); useEffect(() => { if (user) { navigate("/dashboard/stacks", { replace: true }); } }, [user, navigate]); const phraseWords = useMemo(() => { if (fileContent.trim()) { return extractWordsFromText(fileContent); } return phraseInputs.map(normalizeWord).filter(Boolean); }, [fileContent, phraseInputs]); const handlePhraseInputChange = useCallback((index, value) => { setPhraseInputs((prev) => { const next = [...prev]; next[index] = value; return next; }); }, []); const handleFileUpload = useCallback((event) => { const [file] = event.target.files || []; if (!file) { setFileName(""); setFileContent(""); return; } setFileName(file.name); const reader = new FileReader(); reader.onload = (loadEvent) => { const text = String(loadEvent.target?.result || ""); setFileContent(text); setPhraseInputs(Array(PHRASE_WORD_COUNT).fill("")); }; reader.onerror = () => { setFileName(""); setFileContent(""); showToast({ variant: "error", title: "Datei konnte nicht gelesen werden", description: "Bitte lade die Datei erneut hoch." }); }; reader.readAsText(file); }, [showToast]); const handleVerifyPhrase = useCallback(async () => { setVerificationError(""); setVerificationToken(""); const trimmedUsername = username.trim(); if (!trimmedUsername) { setVerificationError("Bitte gib deinen Benutzernamen ein."); return; } const words = phraseWords; const hasFile = Boolean(fileContent.trim()); if (!hasFile && words.length !== PHRASE_WORD_COUNT) { setVerificationError("Bitte gib alle acht Wörter des Sicherheitsschlüssels ein."); return; } if (hasFile && words.length !== PHRASE_WORD_COUNT) { setVerificationError("Die hochgeladene Datei enthält nicht genau acht Wörter."); return; } const payload = buildPhrasePayload(words); if (!payload.canonical) { setVerificationError("Der Sicherheitsschlüssel ist ungültig oder leer."); return; } setVerifying(true); try { const response = await axios.post("/api/auth/recover/verify", { username: trimmedUsername, phrase: payload.canonical, words: payload.words }); const responseToken = response.data?.token; if (!responseToken) { throw new Error("TOKEN_MISSING"); } setVerificationToken(responseToken); showToast({ variant: "success", title: "Sicherheitsschlüssel bestätigt", description: "Du kannst jetzt ein neues Passwort festlegen." }); } catch (error) { const serverError = error.response?.data?.error; if (serverError === "USER_NOT_FOUND") { setVerificationError("Der angegebene Benutzer wurde nicht gefunden."); } else if (serverError === "PHRASE_MISMATCH") { setVerificationError("Der Sicherheitsschlüssel passt nicht zum Benutzer."); } else if (serverError === "PHRASE_NOT_INITIALIZED") { setVerificationError("Für diesen Benutzer ist aktuell kein Sicherheitsschlüssel hinterlegt."); } else if (serverError === "PHRASE_REQUIRED") { setVerificationError("Bitte gib den Sicherheitsschlüssel ein."); } else if (serverError === "USERNAME_REQUIRED") { setVerificationError("Der Benutzername darf nicht leer sein."); } else if (serverError === "INVALID_PASSWORD") { setVerificationError("Der Sicherheitsschlüssel ist ungültig."); } else { setVerificationError("Die Sicherheitsprüfung ist fehlgeschlagen. Bitte versuche es erneut."); } } finally { setVerifying(false); } }, [fileContent, phraseWords, showToast, username]); const handleResetPassword = useCallback(async () => { setResetError(""); if (!verificationToken) { setResetError("Bitte bestätige zuerst deinen Sicherheitsschlüssel."); return; } const trimmedPassword = password.trim(); const trimmedConfirm = confirmPassword.trim(); if (!trimmedPassword || !trimmedConfirm) { setResetError("Bitte gib das neue Passwort zweimal ein."); return; } if (trimmedPassword !== trimmedConfirm) { setResetError("Die Passwörter stimmen nicht überein."); return; } setResetting(true); try { await axios.post("/api/auth/recover/reset", { token: verificationToken, password: trimmedPassword, confirmPassword: trimmedConfirm }); showToast({ variant: "success", title: "Passwort aktualisiert", description: "Du kannst dich jetzt mit deinem neuen Passwort anmelden." }); navigate("/auth/sign-in", { replace: true }); } catch (error) { const serverError = error.response?.data?.error; if (serverError === "TOKEN_INVALID_OR_EXPIRED") { setResetError("Die Wiederherstellungssitzung ist abgelaufen. Bitte starte den Vorgang erneut."); setVerificationToken(""); } else if (serverError === "PASSWORD_TOO_SHORT") { setResetError("Das Passwort muss mindestens 8 Zeichen enthalten."); } else if (serverError === "PASSWORD_REQUIRED") { setResetError("Bitte gib ein neues Passwort ein."); } else if (serverError === "PASSWORD_MISMATCH") { setResetError("Die Passwörter stimmen nicht überein."); } else { setResetError("Das Passwort konnte nicht zurückgesetzt werden. Bitte versuche es erneut."); } } finally { setResetting(false); } }, [confirmPassword, navigate, password, showToast, verificationToken]); const verificationCompleted = Boolean(verificationToken); return (
Passwort zurücksetzen Gib deinen Benutzernamen und den Sicherheitsschlüssel (8 Wörter) ein oder lade die gespeicherte Schlüsseldatei.
Schritt 1: Sicherheitsschlüssel prüfen
Benutzername setUsername(event.target.value)} disabled={verificationCompleted || verifying} className=" !border-t-blue-gray-200 focus:!border-t-gray-900" labelProps={{ className: "before:content-none after:content-none" }} />
Sicherheitsschlüssel eingeben
{phraseInputs.map((value, index) => ( handlePhraseInputChange(index, event.target.value)} disabled={verificationCompleted || verifying || Boolean(fileContent.trim())} className=" !border-t-blue-gray-200 focus:!border-t-gray-900" labelProps={{ className: "before:content-none after:content-none" }} /> ))}
Alternativ: Schlüsseldatei (.txt) hochladen
{fileName && ( Ausgewählte Datei: {fileName} )}
{verificationError && ( {verificationError} )}
{verificationCompleted && ( Die Wiederherstellung bleibt für kurze Zeit aktiv. Bitte setze dein Passwort zeitnah zurück. )}
{/* Schritt 2 */}
Schritt 2: Neues Passwort festlegen {!verificationCompleted && ( Nach erfolgreicher Prüfung des Sicherheitsschlüssels kannst du hier ein neues Passwort vergeben. )} {verificationCompleted && (
Neues Passwort setPassword(event.target.value)} disabled={resetting} className=" !border-t-blue-gray-200 focus:!border-t-gray-900" labelProps={{ className: "before:content-none after:content-none" }} />
Passwort bestätigen setConfirmPassword(event.target.value)} disabled={resetting} className=" !border-t-blue-gray-200 focus:!border-t-gray-900" labelProps={{ className: "before:content-none after:content-none" }} />
{resetError && ( {resetError} )}
)}
navigate("/auth/sign-in")} className="text-sm font-medium text-blue-600 transition hover:text-blue-800" > Zurück zur Anmeldung
); } export default ForgotPassword;