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:
commit
1a60cf55a3
69 changed files with 24471 additions and 0 deletions
479
src/middleware/error.middleware.js
Normal file
479
src/middleware/error.middleware.js
Normal 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue