Initial commit: Shattered Void MMO foundation

- Complete PostgreSQL database schema with 21+ tables
- Express.js server with dual authentication (player/admin)
- WebSocket support for real-time features
- Comprehensive middleware (auth, validation, logging, security)
- Game systems: colonies, resources, fleets, research, factions
- Plugin-based combat architecture
- Admin panel foundation
- Production-ready logging and error handling
- Docker support and CI/CD ready
- Complete project structure following CLAUDE.md patterns

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MegaProxy 2025-08-02 02:13:05 +00:00
commit 1a60cf55a3
69 changed files with 24471 additions and 0 deletions

View file

@ -0,0 +1,479 @@
/**
* 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
};