feat: implement complete Phase 2 frontend foundation with React 18

Major milestone: Frontend implementation complete for Shattered Void MMO

FRONTEND IMPLEMENTATION:
- React 18 + TypeScript + Vite development environment
- Tailwind CSS with custom dark theme for sci-fi aesthetic
- Zustand state management with authentication persistence
- Socket.io WebSocket client with auto-reconnection
- Protected routing with authentication guards
- Responsive design with mobile-first approach

AUTHENTICATION SYSTEM:
- Login/register forms with comprehensive validation
- JWT token management with localStorage persistence
- Password strength validation and user feedback
- Protected routes and authentication guards

CORE GAME INTERFACE:
- Colony management dashboard with real-time updates
- Resource display with live production tracking
- WebSocket integration for real-time game events
- Navigation with connection status indicator
- Toast notifications for user feedback

BACKEND ENHANCEMENTS:
- Complete Research System with technology tree (23 technologies)
- Fleet Management System with ship designs and movement
- Enhanced Authentication with email verification and password reset
- Complete game tick integration for all systems
- Advanced WebSocket events for real-time updates

ARCHITECTURE FEATURES:
- Type-safe TypeScript throughout
- Component-based architecture with reusable UI elements
- API client with request/response interceptors
- Error handling and loading states
- Performance optimized builds with code splitting

Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met)
Ready for: Colony management, fleet operations, research interface
Next: Enhanced gameplay features and admin interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MegaProxy 2025-08-02 18:36:06 +00:00
parent 8d9ef427be
commit d41d1e8125
130 changed files with 33588 additions and 14817 deletions

View file

@ -9,70 +9,70 @@ const logger = require('../utils/logger');
* Custom error classes for better error handling
*/
class ValidationError extends Error {
constructor(message, details = null) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
this.details = details;
}
constructor(message, details = null) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
this.details = details;
}
}
class AuthenticationError extends Error {
constructor(message = 'Authentication failed') {
super(message);
this.name = 'AuthenticationError';
this.statusCode = 401;
}
constructor(message = 'Authentication failed') {
super(message);
this.name = 'AuthenticationError';
this.statusCode = 401;
}
}
class AuthorizationError extends Error {
constructor(message = 'Access denied') {
super(message);
this.name = 'AuthorizationError';
this.statusCode = 403;
}
constructor(message = 'Access denied') {
super(message);
this.name = 'AuthorizationError';
this.statusCode = 403;
}
}
class NotFoundError extends Error {
constructor(message = 'Resource not found') {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
constructor(message = 'Resource not found') {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
class ConflictError extends Error {
constructor(message = 'Resource conflict') {
super(message);
this.name = 'ConflictError';
this.statusCode = 409;
}
constructor(message = 'Resource conflict') {
super(message);
this.name = 'ConflictError';
this.statusCode = 409;
}
}
class RateLimitError extends Error {
constructor(message = 'Rate limit exceeded') {
super(message);
this.name = 'RateLimitError';
this.statusCode = 429;
}
constructor(message = 'Rate limit exceeded') {
super(message);
this.name = 'RateLimitError';
this.statusCode = 429;
}
}
class ServiceError extends Error {
constructor(message = 'Internal service error', originalError = null) {
super(message);
this.name = 'ServiceError';
this.statusCode = 500;
this.originalError = originalError;
}
constructor(message = 'Internal service error', originalError = null) {
super(message);
this.name = 'ServiceError';
this.statusCode = 500;
this.originalError = originalError;
}
}
class DatabaseError extends Error {
constructor(message = 'Database operation failed', originalError = null) {
super(message);
this.name = 'DatabaseError';
this.statusCode = 500;
this.originalError = originalError;
}
constructor(message = 'Database operation failed', originalError = null) {
super(message);
this.name = 'DatabaseError';
this.statusCode = 500;
this.originalError = originalError;
}
}
/**
@ -83,41 +83,41 @@ class DatabaseError extends Error {
* @param {Function} next - Express next function
*/
function errorHandler(error, req, res, next) {
const correlationId = req.correlationId || 'unknown';
const startTime = Date.now();
const correlationId = req.correlationId || 'unknown';
const startTime = Date.now();
// Don't handle if response already sent
if (res.headersSent) {
logger.error('Error occurred after response sent', {
correlationId,
error: error.message,
stack: error.stack
});
return next(error);
}
// Log the error
logError(error, req, correlationId);
// Determine error details
const errorResponse = createErrorResponse(error, req, correlationId);
// Set appropriate headers
res.set({
'Content-Type': 'application/json',
'X-Correlation-ID': correlationId
// Don't handle if response already sent
if (res.headersSent) {
logger.error('Error occurred after response sent', {
correlationId,
error: error.message,
stack: error.stack,
});
return next(error);
}
// Send error response
res.status(errorResponse.statusCode).json(errorResponse.body);
// Log the error
logError(error, req, correlationId);
// Log response time for error handling
const duration = Date.now() - startTime;
logger.info('Error response sent', {
correlationId,
statusCode: errorResponse.statusCode,
duration: `${duration}ms`
});
// Determine error details
const errorResponse = createErrorResponse(error, req, correlationId);
// Set appropriate headers
res.set({
'Content-Type': 'application/json',
'X-Correlation-ID': correlationId,
});
// Send error response
res.status(errorResponse.statusCode).json(errorResponse.body);
// Log response time for error handling
const duration = Date.now() - startTime;
logger.info('Error response sent', {
correlationId,
statusCode: errorResponse.statusCode,
duration: `${duration}ms`,
});
}
/**
@ -127,62 +127,62 @@ function errorHandler(error, req, res, next) {
* @param {string} correlationId - Request correlation ID
*/
function logError(error, req, correlationId) {
const errorInfo = {
correlationId,
name: error.name,
message: error.message,
statusCode: error.statusCode || 500,
method: req.method,
url: req.originalUrl,
path: req.path,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.playerId || req.user?.adminId,
userType: req.user?.type,
timestamp: new Date().toISOString()
};
const errorInfo = {
correlationId,
name: error.name,
message: error.message,
statusCode: error.statusCode || 500,
method: req.method,
url: req.originalUrl,
path: req.path,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.playerId || req.user?.adminId,
userType: req.user?.type,
timestamp: new Date().toISOString(),
};
// Add stack trace for server errors
if (!error.statusCode || error.statusCode >= 500) {
errorInfo.stack = error.stack;
// Add original error if available
if (error.originalError) {
errorInfo.originalError = {
name: error.originalError.name,
message: error.originalError.message,
stack: error.originalError.stack
};
}
}
// Add stack trace for server errors
if (!error.statusCode || error.statusCode >= 500) {
errorInfo.stack = error.stack;
// Add request body for debugging (sanitized)
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
errorInfo.requestBody = sanitizeForLogging(req.body);
// Add original error if available
if (error.originalError) {
errorInfo.originalError = {
name: error.originalError.name,
message: error.originalError.message,
stack: error.originalError.stack,
};
}
}
// Add query parameters
if (Object.keys(req.query).length > 0) {
errorInfo.queryParams = req.query;
}
// Add request body for debugging (sanitized)
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
errorInfo.requestBody = sanitizeForLogging(req.body);
}
// Determine log level
const statusCode = error.statusCode || 500;
if (statusCode >= 500) {
logger.error('Server error occurred', errorInfo);
} else if (statusCode >= 400) {
logger.warn('Client error occurred', errorInfo);
} else {
logger.info('Request completed with error', errorInfo);
}
// Add query parameters
if (Object.keys(req.query).length > 0) {
errorInfo.queryParams = req.query;
}
// Audit sensitive errors
if (shouldAuditError(error, req)) {
logger.audit('Error occurred', {
...errorInfo,
audit: true
});
}
// Determine log level
const statusCode = error.statusCode || 500;
if (statusCode >= 500) {
logger.error('Server error occurred', errorInfo);
} else if (statusCode >= 400) {
logger.warn('Client error occurred', errorInfo);
} else {
logger.info('Request completed with error', errorInfo);
}
// Audit sensitive errors
if (shouldAuditError(error, req)) {
logger.audit('Error occurred', {
...errorInfo,
audit: true,
});
}
}
/**
@ -193,133 +193,133 @@ function logError(error, req, correlationId) {
* @returns {Object} Error response object
*/
function createErrorResponse(error, req, correlationId) {
const statusCode = determineStatusCode(error);
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const statusCode = determineStatusCode(error);
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const baseResponse = {
error: true,
correlationId,
timestamp: new Date().toISOString()
const baseResponse = {
error: true,
correlationId,
timestamp: new Date().toISOString(),
};
// Handle different error types
switch (error.name) {
case 'ValidationError':
return {
statusCode: 400,
body: {
...baseResponse,
type: 'ValidationError',
message: 'Request validation failed',
details: error.details || error.message,
},
};
// Handle different error types
switch (error.name) {
case 'ValidationError':
return {
statusCode: 400,
body: {
...baseResponse,
type: 'ValidationError',
message: 'Request validation failed',
details: error.details || error.message
}
};
case 'AuthenticationError':
return {
statusCode: 401,
body: {
...baseResponse,
type: 'AuthenticationError',
message: isProduction ? 'Authentication required' : error.message,
},
};
case 'AuthenticationError':
return {
statusCode: 401,
body: {
...baseResponse,
type: 'AuthenticationError',
message: isProduction ? 'Authentication required' : error.message
}
};
case 'AuthorizationError':
return {
statusCode: 403,
body: {
...baseResponse,
type: 'AuthorizationError',
message: isProduction ? 'Access denied' : error.message,
},
};
case 'AuthorizationError':
return {
statusCode: 403,
body: {
...baseResponse,
type: 'AuthorizationError',
message: isProduction ? 'Access denied' : error.message
}
};
case 'NotFoundError':
return {
statusCode: 404,
body: {
...baseResponse,
type: 'NotFoundError',
message: error.message || 'Resource not found',
},
};
case 'NotFoundError':
return {
statusCode: 404,
body: {
...baseResponse,
type: 'NotFoundError',
message: error.message || 'Resource not found'
}
};
case 'ConflictError':
return {
statusCode: 409,
body: {
...baseResponse,
type: 'ConflictError',
message: error.message || 'Resource conflict',
},
};
case 'ConflictError':
return {
statusCode: 409,
body: {
...baseResponse,
type: 'ConflictError',
message: error.message || 'Resource conflict'
}
};
case 'RateLimitError':
return {
statusCode: 429,
body: {
...baseResponse,
type: 'RateLimitError',
message: error.message || 'Rate limit exceeded',
retryAfter: error.retryAfter,
},
};
case 'RateLimitError':
return {
statusCode: 429,
body: {
...baseResponse,
type: 'RateLimitError',
message: error.message || 'Rate limit exceeded',
retryAfter: error.retryAfter
}
};
// Database errors
case 'DatabaseError':
case 'SequelizeError':
case 'QueryFailedError':
return {
statusCode: 500,
body: {
...baseResponse,
type: 'DatabaseError',
message: isProduction ? 'Database operation failed' : error.message,
...(isDevelopment && { stack: error.stack }),
},
};
// Database errors
case 'DatabaseError':
case 'SequelizeError':
case 'QueryFailedError':
return {
statusCode: 500,
body: {
...baseResponse,
type: 'DatabaseError',
message: isProduction ? 'Database operation failed' : error.message,
...(isDevelopment && { stack: error.stack })
}
};
// JWT errors
case 'JsonWebTokenError':
case 'TokenExpiredError':
case 'NotBeforeError':
return {
statusCode: 401,
body: {
...baseResponse,
type: 'TokenError',
message: 'Invalid or expired token',
},
};
// JWT errors
case 'JsonWebTokenError':
case 'TokenExpiredError':
case 'NotBeforeError':
return {
statusCode: 401,
body: {
...baseResponse,
type: 'TokenError',
message: 'Invalid or expired token'
}
};
// Multer errors (file upload)
case 'MulterError':
return {
statusCode: 400,
body: {
...baseResponse,
type: 'FileUploadError',
message: getMulterErrorMessage(error),
},
};
// Multer errors (file upload)
case 'MulterError':
return {
statusCode: 400,
body: {
...baseResponse,
type: 'FileUploadError',
message: getMulterErrorMessage(error)
}
};
// Default server error
default:
return {
statusCode: statusCode >= 400 ? statusCode : 500,
body: {
...baseResponse,
type: 'ServerError',
message: isProduction ? 'Internal server error' : error.message,
...(isDevelopment && {
stack: error.stack,
originalError: error.originalError
})
}
};
}
// Default server error
default:
return {
statusCode: statusCode >= 400 ? statusCode : 500,
body: {
...baseResponse,
type: 'ServerError',
message: isProduction ? 'Internal server error' : error.message,
...(isDevelopment && {
stack: error.stack,
originalError: error.originalError,
}),
},
};
}
}
/**
@ -328,33 +328,33 @@ function createErrorResponse(error, req, correlationId) {
* @returns {number} HTTP status code
*/
function determineStatusCode(error) {
// Use explicit status code if available
if (error.statusCode && typeof error.statusCode === 'number') {
return error.statusCode;
}
// Use explicit status code if available
if (error.statusCode && typeof error.statusCode === 'number') {
return error.statusCode;
}
// Use status property if available
if (error.status && typeof error.status === 'number') {
return error.status;
}
// Use status property if available
if (error.status && typeof error.status === 'number') {
return error.status;
}
// Default mappings by error name
const statusMappings = {
'ValidationError': 400,
'CastError': 400,
'JsonWebTokenError': 401,
'TokenExpiredError': 401,
'UnauthorizedError': 401,
'AuthenticationError': 401,
'ForbiddenError': 403,
'AuthorizationError': 403,
'NotFoundError': 404,
'ConflictError': 409,
'MulterError': 400,
'RateLimitError': 429
};
// Default mappings by error name
const statusMappings = {
ValidationError: 400,
CastError: 400,
JsonWebTokenError: 401,
TokenExpiredError: 401,
UnauthorizedError: 401,
AuthenticationError: 401,
ForbiddenError: 403,
AuthorizationError: 403,
NotFoundError: 404,
ConflictError: 409,
MulterError: 400,
RateLimitError: 429,
};
return statusMappings[error.name] || 500;
return statusMappings[error.name] || 500;
}
/**
@ -363,22 +363,22 @@ function determineStatusCode(error) {
* @returns {string} User-friendly error message
*/
function getMulterErrorMessage(error) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
return 'File size too large';
case 'LIMIT_FILE_COUNT':
return 'Too many files uploaded';
case 'LIMIT_FIELD_KEY':
return 'Field name too long';
case 'LIMIT_FIELD_VALUE':
return 'Field value too long';
case 'LIMIT_FIELD_COUNT':
return 'Too many fields';
case 'LIMIT_UNEXPECTED_FILE':
return 'Unexpected file field';
default:
return 'File upload error';
}
switch (error.code) {
case 'LIMIT_FILE_SIZE':
return 'File size too large';
case 'LIMIT_FILE_COUNT':
return 'Too many files uploaded';
case 'LIMIT_FIELD_KEY':
return 'Field name too long';
case 'LIMIT_FIELD_VALUE':
return 'Field value too long';
case 'LIMIT_FIELD_COUNT':
return 'Too many fields';
case 'LIMIT_UNEXPECTED_FILE':
return 'Unexpected file field';
default:
return 'File upload error';
}
}
/**
@ -387,30 +387,30 @@ function getMulterErrorMessage(error) {
* @returns {Object} Sanitized data
*/
function sanitizeForLogging(data) {
if (!data || typeof data !== 'object') return data;
if (!data || typeof data !== 'object') return data;
try {
const sanitized = JSON.parse(JSON.stringify(data));
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization'];
try {
const sanitized = JSON.parse(JSON.stringify(data));
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization'];
function recursiveSanitize(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
function recursiveSanitize(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
Object.keys(obj).forEach(key => {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
recursiveSanitize(obj[key]);
}
});
return obj;
Object.keys(obj).forEach(key => {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
recursiveSanitize(obj[key]);
}
});
return recursiveSanitize(sanitized);
} catch {
return '[SANITIZATION_ERROR]';
return obj;
}
return recursiveSanitize(sanitized);
} catch {
return '[SANITIZATION_ERROR]';
}
}
/**
@ -420,25 +420,25 @@ function sanitizeForLogging(data) {
* @returns {boolean} True if should audit
*/
function shouldAuditError(error, req) {
const statusCode = error.statusCode || 500;
const statusCode = error.statusCode || 500;
// Audit all server errors
if (statusCode >= 500) return true;
// Audit all server errors
if (statusCode >= 500) return true;
// Audit authentication/authorization errors
if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) {
return true;
}
// Audit authentication/authorization errors
if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) {
return true;
}
// Audit admin-related errors
if (req.user?.type === 'admin') return true;
// Audit admin-related errors
if (req.user?.type === 'admin') return true;
// Audit security-related endpoints
if (req.path.includes('/auth/') || req.path.includes('/admin/')) {
return true;
}
// Audit security-related endpoints
if (req.path.includes('/auth/') || req.path.includes('/admin/')) {
return true;
}
return false;
return false;
}
/**
@ -447,9 +447,9 @@ function shouldAuditError(error, req) {
* @returns {Function} Wrapped route handler
*/
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
/**
@ -459,21 +459,21 @@ function asyncHandler(fn) {
* @param {Function} next - Express next function
*/
function notFoundHandler(req, res, next) {
const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`);
next(error);
const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`);
next(error);
}
module.exports = {
errorHandler,
notFoundHandler,
asyncHandler,
// Export error classes
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
RateLimitError,
ServiceError,
DatabaseError
};
errorHandler,
notFoundHandler,
asyncHandler,
// Export error classes
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ConflictError,
RateLimitError,
ServiceError,
DatabaseError,
};