87df61ab15
Run Tests on Branches / Detect Changes (push) Successful in 14s
Run Tests on Branches / Backend Tests (push) Successful in 2m24s
Run Tests on Branches / Frontend Tests (push) Successful in 1m57s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
- Add Mailpit SMTP container to docker-compose for dev email capture - Add nodemailer dependency for email sending - Add password_reset_tokens table (PG + SQLite) - Add POST /api/auth/forgot-username endpoint - Add POST /api/auth/forgot-password endpoint (token-based, 1h expiry) - Add POST /api/auth/reset-password endpoint - Create ForgotPasswordModal component (choose mode → email → sent) - Create ResetPasswordView (token-based new password form) - Add forgot/recovery links to LoginModal - Add i18n translations (ES/CA) for the full flow
110 lines
3.2 KiB
React
110 lines
3.2 KiB
React
import React, { useState, useEffect } from 'react';
|
|
import { useTranslation } from '../i18n';
|
|
import './ResetPasswordView.css';
|
|
|
|
function ResetPasswordView({ token, onReset }) {
|
|
const { t } = useTranslation();
|
|
const [password, setPassword] = useState('');
|
|
const [confirm, setConfirm] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!token) setError(t('reset.invalidToken'));
|
|
}, [token, t]);
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
if (password.length < 8) {
|
|
setError(t('login.passwordError'));
|
|
return;
|
|
}
|
|
if (password !== confirm) {
|
|
setError(t('reset.passwordsDontMatch'));
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const res = await fetch('/api/auth/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, password }),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
setError(data.error || t('reset.error'));
|
|
return;
|
|
}
|
|
setSuccess(true);
|
|
} catch {
|
|
setError(t('login.networkError'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
return (
|
|
<div className="reset-container">
|
|
<div className="reset-card">
|
|
<div className="reset-icon">✅</div>
|
|
<h2>{t('reset.successTitle')}</h2>
|
|
<p>{t('reset.successText')}</p>
|
|
<button type="button" className="reset-btn" onClick={onReset}>
|
|
{t('reset.goToLogin')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="reset-container">
|
|
<div className="reset-card">
|
|
<h2>{t('reset.title')}</h2>
|
|
<p className="reset-sub">{t('reset.subtitle')}</p>
|
|
<form onSubmit={handleSubmit} noValidate>
|
|
<div className="reset-field">
|
|
<label htmlFor="reset-password">{t('reset.newPassword')}</label>
|
|
<input
|
|
id="reset-password"
|
|
type="password"
|
|
value={password}
|
|
onChange={e => setPassword(e.target.value)}
|
|
autoComplete="new-password"
|
|
disabled={loading}
|
|
minLength={8}
|
|
placeholder="········"
|
|
/>
|
|
</div>
|
|
<div className="reset-field">
|
|
<label htmlFor="reset-confirm">{t('reset.confirmPassword')}</label>
|
|
<input
|
|
id="reset-confirm"
|
|
type="password"
|
|
value={confirm}
|
|
onChange={e => setConfirm(e.target.value)}
|
|
autoComplete="new-password"
|
|
disabled={loading}
|
|
minLength={8}
|
|
placeholder="········"
|
|
/>
|
|
</div>
|
|
{error && <p className="reset-error">{error}</p>}
|
|
<button
|
|
type="submit"
|
|
className="reset-btn"
|
|
disabled={loading || !password || !confirm}
|
|
>
|
|
{loading ? t('reset.saving') : t('reset.saveBtn')}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ResetPasswordView;
|