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>
479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
/**
|
|
* Global Error Handling Middleware
|
|
* Comprehensive error handling with proper logging, sanitization, and response formatting
|
|
*/
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
class AuthenticationError extends Error {
|
|
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;
|
|
}
|
|
}
|
|
|
|
class NotFoundError extends Error {
|
|
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;
|
|
}
|
|
}
|
|
|
|
class RateLimitError extends Error {
|
|
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;
|
|
}
|
|
}
|
|
|
|
class DatabaseError extends Error {
|
|
constructor(message = 'Database operation failed', originalError = null) {
|
|
super(message);
|
|
this.name = 'DatabaseError';
|
|
this.statusCode = 500;
|
|
this.originalError = originalError;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main error handling middleware
|
|
* @param {Error} error - Error object
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
* @param {Function} next - Express next function
|
|
*/
|
|
function errorHandler(error, req, res, next) {
|
|
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,
|
|
});
|
|
|
|
// 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`,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log error with appropriate detail level
|
|
* @param {Error} error - Error object
|
|
* @param {Object} req - Express request object
|
|
* @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(),
|
|
};
|
|
|
|
// 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 request body for debugging (sanitized)
|
|
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
|
|
errorInfo.requestBody = sanitizeForLogging(req.body);
|
|
}
|
|
|
|
// Add query parameters
|
|
if (Object.keys(req.query).length > 0) {
|
|
errorInfo.queryParams = req.query;
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create standardized error response
|
|
* @param {Error} error - Error object
|
|
* @param {Object} req - Express request object
|
|
* @param {string} correlationId - Request correlation ID
|
|
* @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 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,
|
|
},
|
|
};
|
|
|
|
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 '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 '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 }),
|
|
},
|
|
};
|
|
|
|
// 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),
|
|
},
|
|
};
|
|
|
|
// 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,
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine HTTP status code from error
|
|
* @param {Error} error - Error object
|
|
* @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 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,
|
|
};
|
|
|
|
return statusMappings[error.name] || 500;
|
|
}
|
|
|
|
/**
|
|
* Get user-friendly message for Multer errors
|
|
* @param {Error} error - Multer 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';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize data for logging (remove sensitive information)
|
|
* @param {Object} data - Data to sanitize
|
|
* @returns {Object} Sanitized data
|
|
*/
|
|
function sanitizeForLogging(data) {
|
|
if (!data || typeof data !== 'object') return data;
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
return recursiveSanitize(sanitized);
|
|
} catch {
|
|
return '[SANITIZATION_ERROR]';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if error should be audited
|
|
* @param {Error} error - Error object
|
|
* @param {Object} req - Express request object
|
|
* @returns {boolean} True if should audit
|
|
*/
|
|
function shouldAuditError(error, req) {
|
|
const statusCode = error.statusCode || 500;
|
|
|
|
// Audit all server errors
|
|
if (statusCode >= 500) 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 security-related endpoints
|
|
if (req.path.includes('/auth/') || req.path.includes('/admin/')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle async errors in route handlers
|
|
* @param {Function} fn - Async route handler function
|
|
* @returns {Function} Wrapped route handler
|
|
*/
|
|
function asyncHandler(fn) {
|
|
return (req, res, next) => {
|
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 404 Not Found handler
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
* @param {Function} next - Express next function
|
|
*/
|
|
function notFoundHandler(req, res, next) {
|
|
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,
|
|
};
|