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

@ -13,123 +13,123 @@ const { performance } = require('perf_hooks');
* @param {Function} next - Express next function
*/
function requestLogger(req, res, next) {
const startTime = performance.now();
const correlationId = req.correlationId;
const startTime = performance.now();
const correlationId = req.correlationId;
// Extract request information
const requestInfo = {
correlationId,
method: req.method,
url: req.originalUrl || req.url,
path: req.path,
query: Object.keys(req.query).length > 0 ? req.query : undefined,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
contentType: req.get('Content-Type'),
contentLength: req.get('Content-Length'),
referrer: req.get('Referrer'),
origin: req.get('Origin'),
timestamp: new Date().toISOString()
};
// Extract request information
const requestInfo = {
correlationId,
method: req.method,
url: req.originalUrl || req.url,
path: req.path,
query: Object.keys(req.query).length > 0 ? req.query : undefined,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
contentType: req.get('Content-Type'),
contentLength: req.get('Content-Length'),
referrer: req.get('Referrer'),
origin: req.get('Origin'),
timestamp: new Date().toISOString(),
};
// Log request start
logger.info('Request started', requestInfo);
// Log request start
logger.info('Request started', requestInfo);
// Store original methods to override
const originalSend = res.send;
const originalJson = res.json;
const originalEnd = res.end;
// Store original methods to override
const originalSend = res.send;
const originalJson = res.json;
const originalEnd = res.end;
let responseBody = null;
let responseSent = false;
let responseBody = null;
let responseSent = false;
// Override res.send to capture response
res.send = function(data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalSend.call(this, data);
};
// Override res.send to capture response
res.send = function (data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalSend.call(this, data);
};
// Override res.json to capture JSON response
res.json = function(data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalJson.call(this, data);
};
// Override res.json to capture JSON response
res.json = function (data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalJson.call(this, data);
};
// Override res.end to capture empty responses
res.end = function(data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalEnd.call(this, data);
};
// Override res.end to capture empty responses
res.end = function (data) {
if (!responseSent) {
responseBody = data;
logResponse();
}
return originalEnd.call(this, data);
};
/**
/**
* Log the response details
*/
function logResponse() {
if (responseSent) return;
responseSent = true;
function logResponse() {
if (responseSent) return;
responseSent = true;
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
const statusCode = res.statusCode;
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
const statusCode = res.statusCode;
const responseInfo = {
correlationId,
method: req.method,
url: req.originalUrl || req.url,
statusCode,
duration: `${duration}ms`,
contentLength: res.get('Content-Length'),
contentType: res.get('Content-Type'),
timestamp: new Date().toISOString()
};
const responseInfo = {
correlationId,
method: req.method,
url: req.originalUrl || req.url,
statusCode,
duration: `${duration}ms`,
contentLength: res.get('Content-Length'),
contentType: res.get('Content-Type'),
timestamp: new Date().toISOString(),
};
// Add user information if available
if (req.user) {
responseInfo.userId = req.user.playerId || req.user.adminId;
responseInfo.userType = req.user.type;
responseInfo.username = req.user.username;
}
// Determine log level based on status code
let logLevel = 'info';
if (statusCode >= 400 && statusCode < 500) {
logLevel = 'warn';
} else if (statusCode >= 500) {
logLevel = 'error';
}
// Add response body for errors (but sanitize sensitive data)
if (statusCode >= 400 && responseBody) {
responseInfo.responseBody = sanitizeResponseBody(responseBody);
}
// Log slow requests as warnings
if (duration > 5000) { // 5 seconds
logLevel = 'warn';
responseInfo.slow = true;
}
logger[logLevel]('Request completed', responseInfo);
// Log audit trail for sensitive operations
if (shouldAudit(req, statusCode)) {
logAuditTrail(req, res, duration, correlationId);
}
// Track performance metrics
trackPerformanceMetrics(req, res, duration);
// Add user information if available
if (req.user) {
responseInfo.userId = req.user.playerId || req.user.adminId;
responseInfo.userType = req.user.type;
responseInfo.username = req.user.username;
}
next();
// Determine log level based on status code
let logLevel = 'info';
if (statusCode >= 400 && statusCode < 500) {
logLevel = 'warn';
} else if (statusCode >= 500) {
logLevel = 'error';
}
// Add response body for errors (but sanitize sensitive data)
if (statusCode >= 400 && responseBody) {
responseInfo.responseBody = sanitizeResponseBody(responseBody);
}
// Log slow requests as warnings
if (duration > 5000) { // 5 seconds
logLevel = 'warn';
responseInfo.slow = true;
}
logger[logLevel]('Request completed', responseInfo);
// Log audit trail for sensitive operations
if (shouldAudit(req, statusCode)) {
logAuditTrail(req, res, duration, correlationId);
}
// Track performance metrics
trackPerformanceMetrics(req, res, duration);
}
next();
}
/**
@ -138,47 +138,47 @@ function requestLogger(req, res, next) {
* @returns {any} Sanitized response body
*/
function sanitizeResponseBody(responseBody) {
if (!responseBody) return responseBody;
if (!responseBody) return responseBody;
try {
let sanitized = responseBody;
// If it's a string, try to parse as JSON
if (typeof responseBody === 'string') {
try {
sanitized = JSON.parse(responseBody);
} catch {
return responseBody; // Return as-is if not JSON
}
}
try {
let sanitized = responseBody;
// Remove sensitive fields
if (typeof sanitized === 'object') {
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash'];
const cloned = JSON.parse(JSON.stringify(sanitized));
function removeSensitiveFields(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') {
removeSensitiveFields(obj[key]);
}
});
return obj;
}
return removeSensitiveFields(cloned);
}
return sanitized;
} catch (error) {
return '[SANITIZATION_ERROR]';
// If it's a string, try to parse as JSON
if (typeof responseBody === 'string') {
try {
sanitized = JSON.parse(responseBody);
} catch {
return responseBody; // Return as-is if not JSON
}
}
// Remove sensitive fields
if (typeof sanitized === 'object') {
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash'];
const cloned = JSON.parse(JSON.stringify(sanitized));
function removeSensitiveFields(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') {
removeSensitiveFields(obj[key]);
}
});
return obj;
}
return removeSensitiveFields(cloned);
}
return sanitized;
} catch (error) {
return '[SANITIZATION_ERROR]';
}
}
/**
@ -188,35 +188,35 @@ function sanitizeResponseBody(responseBody) {
* @returns {boolean} True if should audit
*/
function shouldAudit(req, statusCode) {
// Audit admin actions
if (req.user?.type === 'admin') {
return true;
}
// Audit admin actions
if (req.user?.type === 'admin') {
return true;
}
// Audit authentication attempts
if (req.path.includes('/auth/') || req.path.includes('/login')) {
return true;
}
// Audit authentication attempts
if (req.path.includes('/auth/') || req.path.includes('/login')) {
return true;
}
// Audit failed requests
if (statusCode >= 400) {
return true;
}
// Audit failed requests
if (statusCode >= 400) {
return true;
}
// Audit sensitive game actions
const sensitiveActions = [
'/colonies',
'/fleets',
'/research',
'/messages',
'/profile'
];
// Audit sensitive game actions
const sensitiveActions = [
'/colonies',
'/fleets',
'/research',
'/messages',
'/profile',
];
if (sensitiveActions.some(action => req.path.includes(action)) && req.method !== 'GET') {
return true;
}
if (sensitiveActions.some(action => req.path.includes(action)) && req.method !== 'GET') {
return true;
}
return false;
return false;
}
/**
@ -227,36 +227,36 @@ function shouldAudit(req, statusCode) {
* @param {string} correlationId - Request correlation ID
*/
function logAuditTrail(req, res, duration, correlationId) {
const auditInfo = {
correlationId,
event: 'api_request',
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
};
const auditInfo = {
correlationId,
event: 'api_request',
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString(),
};
// Add user information
if (req.user) {
auditInfo.userId = req.user.playerId || req.user.adminId;
auditInfo.userType = req.user.type;
auditInfo.username = req.user.username;
}
// Add user information
if (req.user) {
auditInfo.userId = req.user.playerId || req.user.adminId;
auditInfo.userType = req.user.type;
auditInfo.username = req.user.username;
}
// Add request parameters for POST/PUT/PATCH requests (sanitized)
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
auditInfo.requestBody = sanitizeRequestBody(req.body);
}
// Add request parameters for POST/PUT/PATCH requests (sanitized)
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
auditInfo.requestBody = sanitizeRequestBody(req.body);
}
// Add query parameters
if (Object.keys(req.query).length > 0) {
auditInfo.queryParams = req.query;
}
// Add query parameters
if (Object.keys(req.query).length > 0) {
auditInfo.queryParams = req.query;
}
logger.audit('Audit trail', auditInfo);
logger.audit('Audit trail', auditInfo);
}
/**
@ -265,22 +265,22 @@ function logAuditTrail(req, res, duration, correlationId) {
* @returns {Object} Sanitized request body
*/
function sanitizeRequestBody(body) {
if (!body || typeof body !== 'object') return body;
if (!body || typeof body !== 'object') return body;
try {
const sensitiveFields = ['password', 'oldPassword', 'newPassword', 'token', 'secret'];
const cloned = JSON.parse(JSON.stringify(body));
sensitiveFields.forEach(field => {
if (cloned[field]) {
cloned[field] = '[REDACTED]';
}
});
try {
const sensitiveFields = ['password', 'oldPassword', 'newPassword', 'token', 'secret'];
const cloned = JSON.parse(JSON.stringify(body));
return cloned;
} catch {
return '[SANITIZATION_ERROR]';
}
sensitiveFields.forEach(field => {
if (cloned[field]) {
cloned[field] = '[REDACTED]';
}
});
return cloned;
} catch {
return '[SANITIZATION_ERROR]';
}
}
/**
@ -290,36 +290,36 @@ function sanitizeRequestBody(body) {
* @param {number} duration - Request duration in milliseconds
*/
function trackPerformanceMetrics(req, res, duration) {
// Only track metrics for non-health check endpoints
if (req.path === '/health') return;
// Only track metrics for non-health check endpoints
if (req.path === '/health') return;
const metrics = {
endpoint: `${req.method} ${req.route?.path || req.path}`,
duration,
statusCode: res.statusCode,
timestamp: Date.now()
};
const metrics = {
endpoint: `${req.method} ${req.route?.path || req.path}`,
duration,
statusCode: res.statusCode,
timestamp: Date.now(),
};
// Log slow requests
if (duration > 1000) { // 1 second
logger.warn('Slow request detected', {
correlationId: req.correlationId,
...metrics,
threshold: '1000ms'
});
}
// Log slow requests
if (duration > 1000) { // 1 second
logger.warn('Slow request detected', {
correlationId: req.correlationId,
...metrics,
threshold: '1000ms',
});
}
// Log very slow requests as errors
if (duration > 10000) { // 10 seconds
logger.error('Very slow request detected', {
correlationId: req.correlationId,
...metrics,
threshold: '10000ms'
});
}
// Log very slow requests as errors
if (duration > 10000) { // 10 seconds
logger.error('Very slow request detected', {
correlationId: req.correlationId,
...metrics,
threshold: '10000ms',
});
}
// TODO: Send metrics to monitoring system (Prometheus, DataDog, etc.)
// This would integrate with your monitoring infrastructure
// TODO: Send metrics to monitoring system (Prometheus, DataDog, etc.)
// This would integrate with your monitoring infrastructure
}
/**
@ -328,15 +328,15 @@ function trackPerformanceMetrics(req, res, duration) {
* @returns {Function} Middleware function
*/
function skipLogging(skipPaths = ['/health', '/favicon.ico']) {
return (req, res, next) => {
const shouldSkip = skipPaths.some(path => req.path === path);
if (shouldSkip) {
return next();
}
return (req, res, next) => {
const shouldSkip = skipPaths.some(path => req.path === path);
return requestLogger(req, res, next);
};
if (shouldSkip) {
return next();
}
return requestLogger(req, res, next);
};
}
/**
@ -347,25 +347,25 @@ function skipLogging(skipPaths = ['/health', '/favicon.ico']) {
* @param {Function} next - Express next function
*/
function errorLogger(error, req, res, next) {
logger.error('Unhandled request error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.playerId || req.user?.adminId,
userType: req.user?.type
});
logger.error('Unhandled request error', {
correlationId: req.correlationId,
error: error.message,
stack: error.stack,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.playerId || req.user?.adminId,
userType: req.user?.type,
});
next(error);
next(error);
}
module.exports = {
requestLogger,
skipLogging,
errorLogger,
sanitizeResponseBody,
sanitizeRequestBody
};
requestLogger,
skipLogging,
errorLogger,
sanitizeResponseBody,
sanitizeRequestBody,
};