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
|
|
@ -9,70 +9,70 @@ 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
constructor(message = 'Database operation failed', originalError = null) {
|
||||
super(message);
|
||||
this.name = 'DatabaseError';
|
||||
this.statusCode = 500;
|
||||
this.originalError = originalError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -83,41 +83,41 @@ class DatabaseError extends Error {
|
|||
* @param {Function} next - Express next function
|
||||
*/
|
||||
function errorHandler(error, req, res, next) {
|
||||
const correlationId = req.correlationId || 'unknown';
|
||||
const startTime = Date.now();
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Send error response
|
||||
res.status(errorResponse.statusCode).json(errorResponse.body);
|
||||
// Log the error
|
||||
logError(error, req, correlationId);
|
||||
|
||||
// Log response time for error handling
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info('Error response sent', {
|
||||
correlationId,
|
||||
statusCode: errorResponse.statusCode,
|
||||
duration: `${duration}ms`
|
||||
});
|
||||
// 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`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,62 +127,62 @@ function errorHandler(error, req, res, next) {
|
|||
* @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()
|
||||
};
|
||||
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 stack trace for server errors
|
||||
if (!error.statusCode || error.statusCode >= 500) {
|
||||
errorInfo.stack = error.stack;
|
||||
|
||||
// Add request body for debugging (sanitized)
|
||||
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
|
||||
errorInfo.requestBody = sanitizeForLogging(req.body);
|
||||
// Add original error if available
|
||||
if (error.originalError) {
|
||||
errorInfo.originalError = {
|
||||
name: error.originalError.name,
|
||||
message: error.originalError.message,
|
||||
stack: error.originalError.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add query parameters
|
||||
if (Object.keys(req.query).length > 0) {
|
||||
errorInfo.queryParams = req.query;
|
||||
}
|
||||
// Add request body for debugging (sanitized)
|
||||
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) {
|
||||
errorInfo.requestBody = sanitizeForLogging(req.body);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Add query parameters
|
||||
if (Object.keys(req.query).length > 0) {
|
||||
errorInfo.queryParams = req.query;
|
||||
}
|
||||
|
||||
// Audit sensitive errors
|
||||
if (shouldAuditError(error, req)) {
|
||||
logger.audit('Error occurred', {
|
||||
...errorInfo,
|
||||
audit: true
|
||||
});
|
||||
}
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -193,133 +193,133 @@ function logError(error, req, correlationId) {
|
|||
* @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 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()
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
// 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 '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 '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 '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 '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,
|
||||
},
|
||||
};
|
||||
|
||||
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 }),
|
||||
},
|
||||
};
|
||||
|
||||
// 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',
|
||||
},
|
||||
};
|
||||
|
||||
// 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),
|
||||
},
|
||||
};
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
// 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,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -328,33 +328,33 @@ function createErrorResponse(error, req, correlationId) {
|
|||
* @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 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;
|
||||
}
|
||||
// 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
|
||||
};
|
||||
// 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;
|
||||
return statusMappings[error.name] || 500;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -363,22 +363,22 @@ function determineStatusCode(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';
|
||||
}
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -387,30 +387,30 @@ function getMulterErrorMessage(error) {
|
|||
* @returns {Object} Sanitized data
|
||||
*/
|
||||
function sanitizeForLogging(data) {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
|
||||
try {
|
||||
const sanitized = JSON.parse(JSON.stringify(data));
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization'];
|
||||
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;
|
||||
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;
|
||||
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 recursiveSanitize(sanitized);
|
||||
} catch {
|
||||
return '[SANITIZATION_ERROR]';
|
||||
return obj;
|
||||
}
|
||||
|
||||
return recursiveSanitize(sanitized);
|
||||
} catch {
|
||||
return '[SANITIZATION_ERROR]';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -420,25 +420,25 @@ function sanitizeForLogging(data) {
|
|||
* @returns {boolean} True if should audit
|
||||
*/
|
||||
function shouldAuditError(error, req) {
|
||||
const statusCode = error.statusCode || 500;
|
||||
const statusCode = error.statusCode || 500;
|
||||
|
||||
// Audit all server errors
|
||||
if (statusCode >= 500) return true;
|
||||
// Audit all server errors
|
||||
if (statusCode >= 500) return true;
|
||||
|
||||
// Audit authentication/authorization errors
|
||||
if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) {
|
||||
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 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;
|
||||
}
|
||||
// Audit security-related endpoints
|
||||
if (req.path.includes('/auth/') || req.path.includes('/admin/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -447,9 +447,9 @@ function shouldAuditError(error, req) {
|
|||
* @returns {Function} Wrapped route handler
|
||||
*/
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -459,21 +459,21 @@ function asyncHandler(fn) {
|
|||
* @param {Function} next - Express next function
|
||||
*/
|
||||
function notFoundHandler(req, res, next) {
|
||||
const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
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
|
||||
};
|
||||
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