215 lines
7.5 KiB
TypeScript
215 lines
7.5 KiB
TypeScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { authAPI } from '../services/api';
|
|
import PasswordInput from '../components/PasswordInput';
|
|
import PasswordStrengthMeter from '../components/PasswordStrengthMeter';
|
|
|
|
const ResetPassword: React.FC = () => {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const { openAuthModal } = useAuth();
|
|
const [error, setError] = useState<string>('');
|
|
const [success, setSuccess] = useState(false);
|
|
const [validating, setValidating] = useState(true);
|
|
const [tokenValid, setTokenValid] = useState(false);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const hasValidated = useRef(false);
|
|
|
|
useEffect(() => {
|
|
const validateToken = async () => {
|
|
// Prevent double execution in React StrictMode
|
|
if (hasValidated.current) {
|
|
return;
|
|
}
|
|
hasValidated.current = true;
|
|
|
|
try {
|
|
const token = searchParams.get('token');
|
|
|
|
if (!token) {
|
|
setError('No reset token provided.');
|
|
setValidating(false);
|
|
return;
|
|
}
|
|
|
|
// Verify the token is valid
|
|
await authAPI.verifyResetToken(token);
|
|
|
|
setTokenValid(true);
|
|
setValidating(false);
|
|
} catch (err: any) {
|
|
console.error('Token validation error:', err);
|
|
const errorData = err.response?.data;
|
|
|
|
if (errorData?.code === 'TOKEN_EXPIRED') {
|
|
setError('Your password reset link has expired. Please request a new one.');
|
|
} else if (errorData?.code === 'TOKEN_INVALID') {
|
|
setError('Invalid password reset link. The link may have already been used or is incorrect.');
|
|
} else {
|
|
setError(errorData?.error || 'Failed to validate reset link. Please try again.');
|
|
}
|
|
|
|
setValidating(false);
|
|
setTokenValid(false);
|
|
}
|
|
};
|
|
|
|
validateToken();
|
|
}, [searchParams]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
// Validate passwords match
|
|
if (newPassword !== confirmPassword) {
|
|
setError('Passwords do not match.');
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
|
|
try {
|
|
const token = searchParams.get('token');
|
|
if (!token) {
|
|
setError('No reset token provided.');
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
await authAPI.resetPassword(token, newPassword);
|
|
|
|
setSuccess(true);
|
|
} catch (err: any) {
|
|
console.error('Password reset error:', err);
|
|
const errorData = err.response?.data;
|
|
|
|
if (errorData?.code === 'TOKEN_EXPIRED') {
|
|
setError('Your reset link has expired. Please request a new one.');
|
|
} else if (errorData?.code === 'TOKEN_INVALID') {
|
|
setError('Invalid reset link. Please request a new one.');
|
|
} else if (errorData?.details) {
|
|
// Validation errors
|
|
const validationErrors = errorData.details.map((d: any) => d.message).join(', ');
|
|
setError(validationErrors);
|
|
} else {
|
|
setError(errorData?.error || 'Failed to reset password. Please try again.');
|
|
}
|
|
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="container">
|
|
<div className="row justify-content-center mt-5">
|
|
<div className="col-md-6">
|
|
<div className="card">
|
|
<div className="card-body py-5">
|
|
{validating ? (
|
|
<div className="text-center">
|
|
<div className="spinner-border text-primary mb-3" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
<h5>Verifying Reset Link...</h5>
|
|
<p className="text-muted">Please wait while we verify your password reset link.</p>
|
|
</div>
|
|
) : success ? (
|
|
<div className="text-center">
|
|
<i className="bi bi-check-circle text-success" style={{ fontSize: '3rem' }}></i>
|
|
<h5 className="mt-3">Password Reset Successfully!</h5>
|
|
<p className="text-muted">
|
|
Your password has been reset. You can now log in with your new password.
|
|
</p>
|
|
<button
|
|
className="btn btn-primary mt-3"
|
|
onClick={() => {
|
|
navigate('/', { replace: true });
|
|
openAuthModal('login');
|
|
}}
|
|
>
|
|
Log In Now
|
|
</button>
|
|
</div>
|
|
) : !tokenValid ? (
|
|
<div className="text-center">
|
|
<i className="bi bi-exclamation-circle text-danger" style={{ fontSize: '3rem' }}></i>
|
|
<h5 className="mt-3">Invalid Reset Link</h5>
|
|
<p className="text-danger">{error}</p>
|
|
<div className="mt-3">
|
|
<button
|
|
className="btn btn-primary me-2"
|
|
onClick={() => {
|
|
navigate('/', { replace: true });
|
|
openAuthModal('forgot-password');
|
|
}}
|
|
>
|
|
Request New Link
|
|
</button>
|
|
<Link to="/" className="btn btn-outline-secondary">
|
|
Return to Home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="text-center mb-4">
|
|
<h4>Reset Your Password</h4>
|
|
<p className="text-muted">Enter your new password below.</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="alert alert-danger" role="alert">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<PasswordInput
|
|
id="newPassword"
|
|
label="New Password"
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
required
|
|
/>
|
|
<div style={{ marginTop: '-0.75rem', marginBottom: '1rem' }}>
|
|
<PasswordStrengthMeter password={newPassword} />
|
|
</div>
|
|
|
|
<PasswordInput
|
|
id="confirmPassword"
|
|
label="Confirm New Password"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
required
|
|
/>
|
|
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary w-100 py-3 mt-3"
|
|
disabled={submitting || !newPassword || !confirmPassword}
|
|
>
|
|
{submitting ? 'Resetting Password...' : 'Reset Password'}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="text-center mt-3">
|
|
<Link to="/" className="text-decoration-none">
|
|
Return to Home
|
|
</Link>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ResetPassword;
|