feat: add forgot password and username recovery flow
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
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
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user