feat: implement comprehensive startup system and fix authentication
Major improvements: - Created startup orchestration system with health monitoring and graceful shutdown - Fixed user registration and login with simplified authentication flow - Rebuilt authentication forms from scratch with direct API integration - Implemented comprehensive debugging and error handling - Added Redis fallback functionality for disabled environments - Fixed CORS configuration for cross-origin frontend requests - Simplified password validation to 6+ characters (removed complexity requirements) - Added toast notifications at app level for better UX feedback - Created comprehensive startup/shutdown scripts with OODA methodology - Fixed database validation and connection issues - Implemented TokenService memory fallback when Redis is disabled Technical details: - New SimpleLoginForm.tsx and SimpleRegisterForm.tsx components - Enhanced CORS middleware with additional allowed origins - Simplified auth validators and removed strict password requirements - Added extensive logging and diagnostic capabilities - Fixed authentication middleware token validation - Implemented graceful Redis error handling throughout the stack - Created modular startup system with configurable health checks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d41d1e8125
commit
e681c446b6
36 changed files with 7719 additions and 183 deletions
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
// Layout components
|
||||
import Layout from './components/layout/Layout';
|
||||
import ProtectedRoute from './components/auth/ProtectedRoute';
|
||||
|
||||
// Auth components
|
||||
import LoginForm from './components/auth/LoginForm';
|
||||
import RegisterForm from './components/auth/RegisterForm';
|
||||
import SimpleLoginForm from './components/auth/SimpleLoginForm';
|
||||
import SimpleRegisterForm from './components/auth/SimpleRegisterForm';
|
||||
|
||||
// Page components
|
||||
import Dashboard from './pages/Dashboard';
|
||||
|
|
@ -20,13 +21,38 @@ const App: React.FC = () => {
|
|||
return (
|
||||
<Router>
|
||||
<div className="App">
|
||||
{/* Toast notifications - available on all pages */}
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#1e293b',
|
||||
color: '#f8fafc',
|
||||
border: '1px solid #334155',
|
||||
},
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: '#22c55e',
|
||||
secondary: '#f8fafc',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: '#ef4444',
|
||||
secondary: '#f8fafc',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Routes>
|
||||
{/* Public routes (redirect to dashboard if authenticated) */}
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={false}>
|
||||
<LoginForm />
|
||||
<SimpleLoginForm />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
|
@ -34,7 +60,7 @@ const App: React.FC = () => {
|
|||
path="/register"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={false}>
|
||||
<RegisterForm />
|
||||
<SimpleRegisterForm />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
272
frontend/src/components/auth/SimpleLoginForm.tsx
Normal file
272
frontend/src/components/auth/SimpleLoginForm.tsx
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, Navigate } from 'react-router-dom';
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
const SimpleLoginForm: React.FC = () => {
|
||||
const [credentials, setCredentials] = useState<LoginCredentials>({
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
// Redirect if already authenticated
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Email validation
|
||||
if (!credentials.email.trim()) {
|
||||
errors.email = 'Email is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) {
|
||||
errors.email = 'Please enter a valid email';
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (!credentials.password) {
|
||||
errors.password = 'Password is required';
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
console.log('Login form submitted with:', { ...credentials, password: '[HIDDEN]' });
|
||||
|
||||
if (!validateForm()) {
|
||||
toast.error('Please fix the validation errors');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Make direct API call
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||
console.log('Making login request to:', `${apiUrl}/api/auth/login`);
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
email: credentials.email.trim().toLowerCase(),
|
||||
password: credentials.password,
|
||||
rememberMe: credentials.rememberMe,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log('Login response status:', response.status);
|
||||
console.log('Login response headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Login response data:', data);
|
||||
|
||||
if (response.ok && data.success) {
|
||||
toast.success('Login successful! Welcome back!');
|
||||
|
||||
// Store auth data manually
|
||||
if (data.data?.token && data.data?.user) {
|
||||
localStorage.setItem('accessToken', data.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user));
|
||||
}
|
||||
|
||||
// Redirect to dashboard
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard';
|
||||
}, 1000);
|
||||
} else {
|
||||
console.error('Login failed:', data);
|
||||
|
||||
if (data.errors && Array.isArray(data.errors)) {
|
||||
// Handle validation errors from backend
|
||||
const backendErrors: Record<string, string> = {};
|
||||
data.errors.forEach((error: any) => {
|
||||
if (error.field && error.message) {
|
||||
backendErrors[error.field] = error.message;
|
||||
}
|
||||
});
|
||||
setValidationErrors(backendErrors);
|
||||
toast.error('Login failed. Please check the errors below.');
|
||||
} else {
|
||||
toast.error(data.message || 'Login failed. Please check your credentials.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error during login:', error);
|
||||
toast.error('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof LoginCredentials, value: string | boolean) => {
|
||||
setCredentials(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear validation error when user starts typing
|
||||
if (typeof value === 'string' && validationErrors[field]) {
|
||||
setValidationErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-dark-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-white">
|
||||
Welcome Back
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-dark-400">
|
||||
Sign in to your Shattered Void account
|
||||
</p>
|
||||
<p className="mt-1 text-center text-sm text-dark-400">
|
||||
Or{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
create a new account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-dark-300">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className={`input-field mt-1 ${
|
||||
validationErrors.email ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
placeholder="Enter your email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
/>
|
||||
{validationErrors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">{validationErrors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-dark-300">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className={`input-field pr-10 ${
|
||||
validationErrors.password ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
placeholder="Enter your password"
|
||||
value={credentials.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-dark-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{validationErrors.password && (
|
||||
<p className="mt-1 text-sm text-red-500">{validationErrors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remember Me & Forgot Password */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-dark-500 rounded bg-dark-700"
|
||||
checked={credentials.rememberMe}
|
||||
onChange={(e) => handleInputChange('rememberMe', e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="rememberMe" className="ml-2 block text-sm text-dark-300">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="btn-primary w-full flex justify-center items-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<div className="loading-spinner w-4 h-4 mr-2"></div>
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-dark-400 text-center">
|
||||
<p>
|
||||
Need help?{' '}
|
||||
<Link to="/support" className="text-primary-600 hover:text-primary-500">
|
||||
Contact Support
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleLoginForm;
|
||||
335
frontend/src/components/auth/SimpleRegisterForm.tsx
Normal file
335
frontend/src/components/auth/SimpleRegisterForm.tsx
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, Navigate } from 'react-router-dom';
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface RegisterCredentials {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const SimpleRegisterForm: React.FC = () => {
|
||||
const [credentials, setCredentials] = useState<RegisterCredentials>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
// Redirect if already authenticated
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Username validation - simple
|
||||
if (!credentials.username.trim()) {
|
||||
errors.username = 'Username is required';
|
||||
} else if (credentials.username.length < 3) {
|
||||
errors.username = 'Username must be at least 3 characters';
|
||||
} else if (credentials.username.length > 30) {
|
||||
errors.username = 'Username must be less than 30 characters';
|
||||
} else if (!/^[a-zA-Z0-9_-]+$/.test(credentials.username)) {
|
||||
errors.username = 'Username can only contain letters, numbers, underscores, and hyphens';
|
||||
}
|
||||
|
||||
// Email validation - simple
|
||||
if (!credentials.email.trim()) {
|
||||
errors.email = 'Email is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) {
|
||||
errors.email = 'Please enter a valid email';
|
||||
}
|
||||
|
||||
// Password validation - simplified (6+ characters)
|
||||
if (!credentials.password) {
|
||||
errors.password = 'Password is required';
|
||||
} else if (credentials.password.length < 6) {
|
||||
errors.password = 'Password must be at least 6 characters';
|
||||
} else if (credentials.password.length > 128) {
|
||||
errors.password = 'Password must be less than 128 characters';
|
||||
}
|
||||
|
||||
// Confirm password
|
||||
if (!credentials.confirmPassword) {
|
||||
errors.confirmPassword = 'Please confirm your password';
|
||||
} else if (credentials.password !== credentials.confirmPassword) {
|
||||
errors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
console.log('Form submitted with:', { ...credentials, password: '[HIDDEN]' });
|
||||
|
||||
if (!validateForm()) {
|
||||
toast.error('Please fix the validation errors');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Make direct API call instead of using auth store
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||
console.log('Making request to:', `${apiUrl}/api/auth/register`);
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
username: credentials.username.trim(),
|
||||
email: credentials.email.trim().toLowerCase(),
|
||||
password: credentials.password,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Response data:', data);
|
||||
|
||||
if (response.ok && data.success) {
|
||||
toast.success('Registration successful! Welcome to Shattered Void!');
|
||||
|
||||
// Store auth data manually since we're bypassing the store
|
||||
if (data.data?.token && data.data?.user) {
|
||||
localStorage.setItem('accessToken', data.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user));
|
||||
}
|
||||
|
||||
// Redirect to dashboard
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard';
|
||||
}, 1000);
|
||||
} else {
|
||||
console.error('Registration failed:', data);
|
||||
|
||||
if (data.errors && Array.isArray(data.errors)) {
|
||||
// Handle validation errors from backend
|
||||
const backendErrors: Record<string, string> = {};
|
||||
data.errors.forEach((error: any) => {
|
||||
if (error.field && error.message) {
|
||||
backendErrors[error.field] = error.message;
|
||||
}
|
||||
});
|
||||
setValidationErrors(backendErrors);
|
||||
toast.error('Registration failed. Please check the errors below.');
|
||||
} else {
|
||||
toast.error(data.message || 'Registration failed. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error during registration:', error);
|
||||
toast.error('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof RegisterCredentials, value: string) => {
|
||||
setCredentials(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear validation error when user starts typing
|
||||
if (validationErrors[field]) {
|
||||
setValidationErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-dark-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-white">
|
||||
Join Shattered Void
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-dark-400">
|
||||
Create your account and start your galactic journey
|
||||
</p>
|
||||
<p className="mt-1 text-center text-sm text-dark-400">
|
||||
Or{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
sign in to your existing account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
{/* Username */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-dark-300">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
className={`input-field mt-1 ${
|
||||
validationErrors.username ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
placeholder="Choose a username (3-30 characters)"
|
||||
value={credentials.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||
/>
|
||||
{validationErrors.username && (
|
||||
<p className="mt-1 text-sm text-red-500">{validationErrors.username}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-dark-300">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className={`input-field mt-1 ${
|
||||
validationErrors.email ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
placeholder="Enter your email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
/>
|
||||
{validationErrors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">{validationErrors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-dark-300">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`input-field pr-10 ${
|
||||
validationErrors.password ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
placeholder="Create a password (6+ characters)"
|
||||
value={credentials.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-dark-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{validationErrors.password && (
|
||||
<p className="mt-1 text-sm text-red-500">{validationErrors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-dark-300">
|
||||
Confirm Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`input-field pr-10 ${
|
||||
validationErrors.confirmPassword ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
placeholder="Confirm your password"
|
||||
value={credentials.confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-dark-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{validationErrors.confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-500">{validationErrors.confirmPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="btn-primary w-full flex justify-center items-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<div className="loading-spinner w-4 h-4 mr-2"></div>
|
||||
Creating account...
|
||||
</>
|
||||
) : (
|
||||
'Create account'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-dark-400 text-center">
|
||||
<p>Password requirements: 6-128 characters (no complexity requirements)</p>
|
||||
<p className="mt-2">
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link to="/terms" className="text-primary-600 hover:text-primary-500">
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link to="/privacy" className="text-primary-600 hover:text-primary-500">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleRegisterForm;
|
||||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import { Outlet } from 'react-router-dom';
|
||||
import Navigation from './Navigation';
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
const Layout: React.FC = () => {
|
||||
// Initialize WebSocket connection for authenticated users
|
||||
|
|
@ -44,31 +43,6 @@ const Layout: React.FC = () => {
|
|||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Toast notifications */}
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#1e293b',
|
||||
color: '#f8fafc',
|
||||
border: '1px solid #334155',
|
||||
},
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: '#22c55e',
|
||||
secondary: '#f8fafc',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: '#ef4444',
|
||||
secondary: '#f8fafc',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue