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:
MegaProxy 2025-08-03 12:53:25 +00:00
parent d41d1e8125
commit e681c446b6
36 changed files with 7719 additions and 183 deletions

View file

@ -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>
}
/>

View 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;

View 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;

View file

@ -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>
);
};