frontend_nyla/src/pages/Reset.tsx
2026-04-17 11:50:25 +02:00

232 lines
8.4 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import styles from './Reset.module.css';
import { usePasswordReset } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext';
function Reset() {
const { t } = useLanguage();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { resetPassword, isLoading, error } = usePasswordReset();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordFocused, setPasswordFocused] = useState(false);
const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [tokenError, setTokenError] = useState<string | null>(null);
// Get token from URL
const token = searchParams.get('token');
// Set page title and generate CSRF token
useEffect(() => {
document.title = "PowerOn AI Platform - Neues Passwort setzen";
generateAndStoreCSRFToken();
// Validate token exists and format
if (!token) {
setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'));
} else if (!_isValidUUID(token)) {
setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'));
}
}, [token, t]);
const _isValidUUID = (str: string): boolean => {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
};
const validateForm = (): boolean => {
if (!password || password.length < 8) {
setValidationError(t('Passwort muss mindestens 8 Zeichen lang sein.'));
return false;
}
if (password !== confirmPassword) {
setValidationError(t('Die Passwörter stimmen nicht überein.'));
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setValidationError(null);
if (!validateForm()) {
return;
}
if (!token) {
setValidationError(t('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.'));
return;
}
try {
await resetPassword(token, password);
setSuccessMessage(t('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet…'));
// Redirect to login after delay
setTimeout(() => {
navigate('/login', {
state: {
passwordReset: true,
message: t('Passwort erfolgreich geändert. Bitte melden Sie sich an.')
}
});
}, 3000);
} catch (err: any) {
// Error is already set by the hook
const errorMessage = err?.response?.data?.detail || err?.message || t('Passwort-Zurücksetzung fehlgeschlagen.');
if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) {
setValidationError(t('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.'));
} else {
setValidationError(errorMessage);
}
}
};
// Show token error if invalid
if (tokenError) {
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"
alt="PowerOn"
className={styles.logoImage}
/>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>{t('Neues Passwort setzen')}</h2>
<div className={styles.loginForm}>
<div className={styles.error}>{tokenError}</div>
<div className={styles.registerLink}>
<button
className={styles.textButton}
onClick={() => navigate("/password-reset-request")}
>
{t('Neuen Reset-Link anfordern')}
</button>
</div>
<div className={styles.registerLink}>
<span>{t('Oder zurück zum')}</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
{t('Login')}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"
alt="PowerOn"
className={styles.logoImage}
/>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>{t('Neues Passwort setzen')}</h2>
<div className={styles.loginForm}>
{(validationError || error) && (
<div className={styles.error}>{validationError || error}</div>
)}
{successMessage && (
<div className={styles.success}>{successMessage}</div>
)}
{!successMessage && (
<form onSubmit={handleSubmit}>
<div className={styles.passwordHint}>{t('Mindestens 8 Zeichen')}</div>
<div className={styles.floatingLabelInput}>
<input
type="password"
placeholder=" "
value={password}
onChange={(e) => {
setPassword(e.target.value);
setValidationError(null);
}}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
autoComplete="new-password"
/>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('Neues Passwort')}</label>
</div>
<div className={styles.floatingLabelInput}>
<input
type="password"
placeholder=" "
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
setValidationError(null);
}}
onFocus={() => setConfirmPasswordFocused(true)}
onBlur={() => setConfirmPasswordFocused(false)}
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
autoComplete="new-password"
/>
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>{t('Passwort bestätigen')}</label>
</div>
<button
type="submit"
className={`${styles.button} ${styles.loginButton}`}
disabled={isLoading}
>
{isLoading ? t('Wird gespeichert…') : t('Passwort setzen')}
</button>
</form>
)}
<div className={styles.registerLink}>
<span>{t('Zurück zum')}</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
>
{t('Login')}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Reset;