Implement authentication features with login, registration, and user management; update service configurations for development and production environments.

This commit is contained in:
2026-01-23 01:50:31 +00:00
parent de14c646fa
commit 8e92c28272
9 changed files with 637 additions and 178 deletions

View File

@@ -0,0 +1,255 @@
import React, { useState } from 'react';
import Modal from './Modal';
import Spinner from './common/Spinner';
import { login, register } from '../client/auth-client';
const VIEW = {
LOGIN: 'login',
REGISTER: 'register',
};
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 resetForm = () => {
setLoginUsername('');
setLoginPassword('');
setRegisterUsername('');
setRegisterEmail('');
setRegisterPassword('');
setRegisterConfirmPassword('');
setError('');
};
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');
}
};
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' : 'Create Account'}
</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>
</form>
) : (
<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>
)}
</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>
) : (
<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>
)}
</Modal.Footer>
</Modal>
);
}