feat: add password reset functionality and email notifications
- Implemented forgot password and reset password routes in the backend. - Added email sending capabilities using Nodemailer for password reset requests. - Created ResetPassword page in the frontend for users to reset their passwords. - Updated user model to include reset token and expiry fields. - Integrated hiscores API with caching mechanism for improved performance. - Enhanced authentication modal to include forgot password option. - Updated environment configuration for SMTP settings.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import Modal from './Modal';
|
||||
import Spinner from './common/Spinner';
|
||||
import { login, register } from '../client/auth-client';
|
||||
import { login, register, forgotPassword } from '../client/auth-client';
|
||||
|
||||
const VIEW = {
|
||||
LOGIN: 'login',
|
||||
REGISTER: 'register',
|
||||
FORGOT_PASSWORD: 'forgot_password',
|
||||
};
|
||||
|
||||
export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
@@ -21,6 +22,9 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
const [registerPassword, setRegisterPassword] = useState('');
|
||||
const [registerConfirmPassword, setRegisterConfirmPassword] = useState('');
|
||||
|
||||
const [forgotEmail, setForgotEmail] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
|
||||
const resetForm = () => {
|
||||
setLoginUsername('');
|
||||
setLoginPassword('');
|
||||
@@ -28,7 +32,9 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
setRegisterEmail('');
|
||||
setRegisterPassword('');
|
||||
setRegisterConfirmPassword('');
|
||||
setForgotEmail('');
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -99,6 +105,28 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = async e => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
|
||||
if (!forgotEmail) {
|
||||
setError('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const result = await forgotPassword(forgotEmail);
|
||||
setIsLoading(false);
|
||||
|
||||
if (result.success) {
|
||||
setSuccessMessage(result.value.message);
|
||||
setForgotEmail('');
|
||||
} else {
|
||||
setError(result.error || 'Failed to send reset email');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -107,11 +135,13 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
className='w-96 shadow shadow-primary rounded-md bg-primary-alt'
|
||||
>
|
||||
<Modal.Header className='text-center small-caps tracking-wide text-xl text-accent font-semibold'>
|
||||
{view === VIEW.LOGIN ? 'Login' : 'Create Account'}
|
||||
{view === VIEW.LOGIN && 'Login'}
|
||||
{view === VIEW.REGISTER && 'Create Account'}
|
||||
{view === VIEW.FORGOT_PASSWORD && 'Reset Password'}
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body className='text-primary text-sm'>
|
||||
{view === VIEW.LOGIN ? (
|
||||
{view === VIEW.LOGIN && (
|
||||
<form onSubmit={handleLogin} className='m-4 flex flex-col gap-3'>
|
||||
<label htmlFor='login-username' className='flex flex-col gap-1'>
|
||||
<span className='text-secondary text-xs'>Username</span>
|
||||
@@ -154,8 +184,18 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
'Login'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='text-accent text-xs hover:underline'
|
||||
onClick={() => switchView(VIEW.FORGOT_PASSWORD)}
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{view === VIEW.REGISTER && (
|
||||
<form onSubmit={handleRegister} className='m-4 flex flex-col gap-3'>
|
||||
<label htmlFor='register-username' className='flex flex-col gap-1'>
|
||||
<span className='text-secondary text-xs'>Username</span>
|
||||
@@ -230,17 +270,54 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{view === VIEW.FORGOT_PASSWORD && (
|
||||
<form onSubmit={handleForgotPassword} className='m-4 flex flex-col gap-3'>
|
||||
<p className='text-secondary text-xs'>
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
|
||||
<label htmlFor='forgot-email' className='flex flex-col gap-1'>
|
||||
<span className='text-secondary text-xs'>Email</span>
|
||||
<input
|
||||
id='forgot-email'
|
||||
name='email'
|
||||
type='email'
|
||||
className='input-primary text-sm form-input'
|
||||
placeholder='Enter your email'
|
||||
value={forgotEmail}
|
||||
onChange={e => setForgotEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
autoComplete='email'
|
||||
/>
|
||||
</label>
|
||||
|
||||
{error && <div className='text-error text-xs text-center'>{error}</div>}
|
||||
{successMessage && <div className='text-success text-xs text-center'>{successMessage}</div>}
|
||||
|
||||
<button type='submit' className='button-filled py-2 mt-2' disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center justify-center gap-2'>
|
||||
<Spinner size={Spinner.SIZE.sm} /> Sending...
|
||||
</span>
|
||||
) : (
|
||||
'Send Reset Link'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer className='text-center text-sm'>
|
||||
{view === VIEW.LOGIN ? (
|
||||
{view === VIEW.LOGIN && (
|
||||
<p className='text-secondary py-2'>
|
||||
Don't have an account?{' '}
|
||||
<button type='button' className='text-accent hover:underline' onClick={() => switchView(VIEW.REGISTER)}>
|
||||
Register
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
)}
|
||||
{view === VIEW.REGISTER && (
|
||||
<p className='text-secondary py-2'>
|
||||
Already have an account?{' '}
|
||||
<button type='button' className='text-accent hover:underline' onClick={() => switchView(VIEW.LOGIN)}>
|
||||
@@ -248,6 +325,14 @@ export default function AuthModal({ isOpen, setIsOpen, onAuthSuccess }) {
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
{view === VIEW.FORGOT_PASSWORD && (
|
||||
<p className='text-secondary py-2'>
|
||||
Remember your password?{' '}
|
||||
<button type='button' className='text-accent hover:underline' onClick={() => switchView(VIEW.LOGIN)}>
|
||||
Login
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user