232 lines
8.4 KiB
TypeScript
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', padding: '1rem 1rem 0' }}>
|
|
<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', padding: '1rem 1rem 0' }}>
|
|
<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;
|