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:
parent
8d9ef427be
commit
d41d1e8125
130 changed files with 33588 additions and 14817 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue