Files
leagues-tools/os-league-tools-master/src/components/AuthModal.js
Sonderau 95063d4066 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.
2026-02-03 23:37:47 +00:00

341 lines
11 KiB
JavaScript

import React, { useState } from 'react';
import Modal from './Modal';
import Spinner from './common/Spinner';
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 }) {
const [view, setView] = useState(VIEW.LOGIN);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [loginUsername, setLoginUsername] = useState('');
const [loginPassword, setLoginPassword] = useState('');
const [registerUsername, setRegisterUsername] = useState('');
const [registerEmail, setRegisterEmail] = useState('');
const [registerPassword, setRegisterPassword] = useState('');
const [registerConfirmPassword, setRegisterConfirmPassword] = useState('');
const [forgotEmail, setForgotEmail] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const resetForm = () => {
setLoginUsername('');
setLoginPassword('');
setRegisterUsername('');
setRegisterEmail('');
setRegisterPassword('');
setRegisterConfirmPassword('');
setForgotEmail('');
setError('');
setSuccessMessage('');
};
const handleClose = () => {
resetForm();
setView(VIEW.LOGIN);
};
const switchView = newView => {
resetForm();
setView(newView);
};
const handleLogin = async e => {
e.preventDefault();
setError('');
if (!loginUsername || !loginPassword) {
setError('Please fill in all fields');
return;
}
setIsLoading(true);
const result = await login(loginUsername, loginPassword);
setIsLoading(false);
if (result.success) {
resetForm();
setIsOpen(false);
if (onAuthSuccess) {
onAuthSuccess(result.value);
}
} else {
setError(result.error || 'Login failed');
}
};
const handleRegister = async e => {
e.preventDefault();
setError('');
if (!registerUsername || !registerEmail || !registerPassword || !registerConfirmPassword) {
setError('Please fill in all fields');
return;
}
if (registerPassword !== registerConfirmPassword) {
setError('Passwords do not match');
return;
}
if (registerPassword.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setIsLoading(true);
const result = await register(registerUsername, registerEmail, registerPassword);
setIsLoading(false);
if (result.success) {
resetForm();
setIsOpen(false);
if (onAuthSuccess) {
onAuthSuccess(result.value);
}
} else {
setError(result.error || 'Registration failed');
}
};
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}
setIsOpen={setIsOpen}
onClose={handleClose}
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'}
{view === VIEW.REGISTER && 'Create Account'}
{view === VIEW.FORGOT_PASSWORD && 'Reset Password'}
</Modal.Header>
<Modal.Body className='text-primary text-sm'>
{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>
<input
id='login-username'
name='username'
type='text'
className='input-primary text-sm form-input'
placeholder='Enter username'
value={loginUsername}
onChange={e => setLoginUsername(e.target.value)}
disabled={isLoading}
autoComplete='username'
/>
</label>
<label htmlFor='login-password' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Password</span>
<input
id='login-password'
name='password'
type='password'
className='input-primary text-sm form-input'
placeholder='Enter password'
value={loginPassword}
onChange={e => setLoginPassword(e.target.value)}
disabled={isLoading}
autoComplete='current-password'
/>
</label>
{error && <div className='text-error text-xs text-center'>{error}</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} /> Logging in...
</span>
) : (
'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>
<input
id='register-username'
name='username'
type='text'
className='input-primary text-sm form-input'
placeholder='Choose a username'
value={registerUsername}
onChange={e => setRegisterUsername(e.target.value)}
disabled={isLoading}
autoComplete='username'
/>
</label>
<label htmlFor='register-email' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Email</span>
<input
id='register-email'
name='email'
type='email'
className='input-primary text-sm form-input'
placeholder='Enter your email'
value={registerEmail}
onChange={e => setRegisterEmail(e.target.value)}
disabled={isLoading}
autoComplete='email'
/>
</label>
<label htmlFor='register-password' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Password</span>
<input
id='register-password'
name='new-password'
type='password'
className='input-primary text-sm form-input'
placeholder='Create a password'
value={registerPassword}
onChange={e => setRegisterPassword(e.target.value)}
disabled={isLoading}
autoComplete='new-password'
/>
</label>
<label htmlFor='register-confirm-password' className='flex flex-col gap-1'>
<span className='text-secondary text-xs'>Confirm Password</span>
<input
id='register-confirm-password'
name='confirm-password'
type='password'
className='input-primary text-sm form-input'
placeholder='Confirm your password'
value={registerConfirmPassword}
onChange={e => setRegisterConfirmPassword(e.target.value)}
disabled={isLoading}
autoComplete='new-password'
/>
</label>
{error && <div className='text-error text-xs text-center'>{error}</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} /> Creating account...
</span>
) : (
'Create Account'
)}
</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&apos;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 && (
<p className='text-secondary py-2'>
Don&apos;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)}>
Login
</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>
);
}