Initial commit: Inventory Barcode System

This commit is contained in:
2025-07-22 20:24:51 -04:00
commit 511b01748d
63 changed files with 26932 additions and 0 deletions

248
middleware/errorHandler.js Normal file
View File

@ -0,0 +1,248 @@
const logger = require('../utils/logger');
/**
* Error types and their corresponding HTTP status codes
*/
const ErrorTypes = {
VALIDATION_ERROR: 'ValidationError',
NOT_FOUND_ERROR: 'NotFoundError',
CONFLICT_ERROR: 'ConflictError',
UNAUTHORIZED_ERROR: 'UnauthorizedError',
FORBIDDEN_ERROR: 'ForbiddenError',
RATE_LIMIT_ERROR: 'RateLimitError',
FILE_UPLOAD_ERROR: 'FileUploadError',
DATABASE_ERROR: 'DatabaseError',
EXTERNAL_SERVICE_ERROR: 'ExternalServiceError',
BUSINESS_LOGIC_ERROR: 'BusinessLogicError'
};
/**
* Custom error class for application-specific errors
*/
class AppError extends Error {
constructor(message, type = 'ApplicationError', statusCode = 500, isOperational = true, details = null) {
super(message);
this.name = this.constructor.name;
this.type = type;
this.statusCode = statusCode;
this.isOperational = isOperational;
this.details = details;
this.timestamp = new Date().toISOString();
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Create specific error types
*/
class ValidationError extends AppError {
constructor(message, details = null) {
super(message, ErrorTypes.VALIDATION_ERROR, 400, true, details);
}
}
class NotFoundError extends AppError {
constructor(message, details = null) {
super(message, ErrorTypes.NOT_FOUND_ERROR, 404, true, details);
}
}
class ConflictError extends AppError {
constructor(message, details = null) {
super(message, ErrorTypes.CONFLICT_ERROR, 409, true, details);
}
}
class DatabaseError extends AppError {
constructor(message, details = null) {
super(message, ErrorTypes.DATABASE_ERROR, 500, true, details);
}
}
class BusinessLogicError extends AppError {
constructor(message, details = null) {
super(message, ErrorTypes.BUSINESS_LOGIC_ERROR, 422, true, details);
}
}
/**
* Error response formatter
*/
const formatErrorResponse = (error, req) => {
const isDevelopment = process.env.NODE_ENV !== 'production';
const baseResponse = {
success: false,
error: error.type || 'InternalServerError',
message: error.message || 'An unexpected error occurred',
timestamp: error.timestamp || new Date().toISOString(),
path: req.originalUrl,
method: req.method
};
// Add details for operational errors
if (error.isOperational && error.details) {
baseResponse.details = error.details;
}
// Add stack trace in development
if (isDevelopment && error.stack) {
baseResponse.stack = error.stack;
}
// Add request ID if available
if (req.requestId) {
baseResponse.requestId = req.requestId;
}
return baseResponse;
};
/**
* Determine if error should be retried
*/
const isRetryableError = (error) => {
const retryableTypes = [
ErrorTypes.DATABASE_ERROR,
ErrorTypes.EXTERNAL_SERVICE_ERROR
];
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
return retryableTypes.includes(error.type) ||
retryableCodes.includes(error.code) ||
(error.statusCode >= 500 && error.statusCode < 600);
};
/**
* Get user-friendly error message
*/
const getUserFriendlyMessage = (error) => {
const friendlyMessages = {
[ErrorTypes.VALIDATION_ERROR]: 'The provided data is invalid. Please check your input and try again.',
[ErrorTypes.NOT_FOUND_ERROR]: 'The requested resource could not be found.',
[ErrorTypes.CONFLICT_ERROR]: 'This operation conflicts with existing data. Please check for duplicates.',
[ErrorTypes.UNAUTHORIZED_ERROR]: 'Authentication is required to access this resource.',
[ErrorTypes.FORBIDDEN_ERROR]: 'You do not have permission to perform this action.',
[ErrorTypes.RATE_LIMIT_ERROR]: 'Too many requests. Please wait a moment before trying again.',
[ErrorTypes.FILE_UPLOAD_ERROR]: 'There was a problem with the uploaded file. Please check the file format and size.',
[ErrorTypes.DATABASE_ERROR]: 'A database error occurred. Please try again later.',
[ErrorTypes.EXTERNAL_SERVICE_ERROR]: 'An external service is temporarily unavailable. Please try again later.',
[ErrorTypes.BUSINESS_LOGIC_ERROR]: 'This operation cannot be completed due to business rules.'
};
return friendlyMessages[error.type] || error.message || 'An unexpected error occurred. Please try again later.';
};
/**
* Main error handling middleware
*/
const errorHandler = (error, req, res, next) => {
// Log the error with context
const errorContext = {
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
body: req.method !== 'GET' ? req.body : undefined,
params: req.params,
query: req.query,
requestId: req.requestId
};
logger.logError(error, errorContext);
// Handle specific error types
let appError = error;
// Convert known errors to AppError instances
if (error.name === 'ValidationError') {
appError = new ValidationError(error.message, error.details);
} else if (error.name === 'CastError') {
appError = new ValidationError('Invalid data format provided');
} else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
appError = new ConflictError('A record with this information already exists');
} else if (error.code === 'SQLITE_CONSTRAINT') {
appError = new ValidationError('Data constraint violation');
} else if (error.code === 'ENOENT') {
appError = new NotFoundError('Requested file or resource not found');
} else if (error.code === 'LIMIT_FILE_SIZE') {
appError = new ValidationError('File size exceeds the maximum allowed limit');
} else if (error.code === 'LIMIT_UNEXPECTED_FILE') {
appError = new ValidationError('Unexpected file upload');
} else if (!error.isOperational) {
// Convert unknown errors to generic AppError
appError = new AppError(
process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: error.message,
'InternalServerError',
500,
false
);
}
// Format response
const errorResponse = formatErrorResponse(appError, req);
// Override message with user-friendly version for production
if (process.env.NODE_ENV === 'production') {
errorResponse.message = getUserFriendlyMessage(appError);
}
// Add retry information for retryable errors
if (isRetryableError(appError)) {
errorResponse.retryable = true;
errorResponse.retryAfter = 5; // seconds
}
// Send error response
res.status(appError.statusCode || 500).json(errorResponse);
};
/**
* 404 handler for unmatched routes
*/
const notFoundHandler = (req, res, next) => {
const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`);
next(error);
};
/**
* Async error wrapper to catch async errors in route handlers
*/
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
/**
* Request timeout handler
*/
const timeoutHandler = (timeout = 30000) => {
return (req, res, next) => {
req.setTimeout(timeout, () => {
const error = new AppError('Request timeout', 'TimeoutError', 408);
next(error);
});
next();
};
};
module.exports = {
ErrorTypes,
AppError,
ValidationError,
NotFoundError,
ConflictError,
DatabaseError,
BusinessLogicError,
errorHandler,
notFoundHandler,
asyncHandler,
timeoutHandler,
isRetryableError,
getUserFriendlyMessage
};

179
middleware/requestLogger.js Normal file
View File

@ -0,0 +1,179 @@
const logger = require('../utils/logger');
const { v4: uuidv4 } = require('uuid');
/**
* Request logging middleware
* Logs all HTTP requests with timing and response information
*/
const requestLogger = (req, res, next) => {
// Generate unique request ID
req.requestId = uuidv4();
// Record start time
const startTime = Date.now();
// Log incoming request
logger.info('Incoming Request', {
requestId: req.requestId,
method: req.method,
url: req.originalUrl,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.get('User-Agent'),
contentType: req.get('Content-Type'),
contentLength: req.get('Content-Length'),
referer: req.get('Referer'),
body: req.method !== 'GET' && req.body ? sanitizeBody(req.body) : undefined,
query: Object.keys(req.query).length > 0 ? req.query : undefined,
params: Object.keys(req.params).length > 0 ? req.params : undefined
});
// Override res.json to capture response data
const originalJson = res.json;
res.json = function(data) {
res.responseData = data;
return originalJson.call(this, data);
};
// Override res.send to capture response data
const originalSend = res.send;
res.send = function(data) {
if (!res.responseData) {
res.responseData = data;
}
return originalSend.call(this, data);
};
// Log response when request finishes
res.on('finish', () => {
const responseTime = Date.now() - startTime;
const logData = {
requestId: req.requestId,
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
responseTime: `${responseTime}ms`,
contentLength: res.get('Content-Length') || 0,
ip: req.ip || req.connection.remoteAddress
};
// Add response data for errors or debug mode
if (res.statusCode >= 400 || process.env.LOG_LEVEL === 'debug') {
logData.responseData = sanitizeResponseData(res.responseData);
}
// Log based on status code
if (res.statusCode >= 500) {
logger.error('Request Completed with Server Error', logData);
} else if (res.statusCode >= 400) {
logger.warn('Request Completed with Client Error', logData);
} else {
logger.http('Request Completed Successfully', logData);
}
// Log slow requests
if (responseTime > 1000) {
logger.warn('Slow Request Detected', {
...logData,
threshold: '1000ms'
});
}
});
// Log request errors
res.on('error', (error) => {
const responseTime = Date.now() - startTime;
logger.error('Request Error', {
requestId: req.requestId,
method: req.method,
url: req.originalUrl,
error: error.message,
responseTime: `${responseTime}ms`,
ip: req.ip || req.connection.remoteAddress
});
});
next();
};
/**
* Sanitize request body to remove sensitive information
*/
const sanitizeBody = (body) => {
if (!body || typeof body !== 'object') {
return body;
}
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
const sanitized = { ...body };
Object.keys(sanitized).forEach(key => {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
};
/**
* Sanitize response data to remove sensitive information
*/
const sanitizeResponseData = (data) => {
if (!data) return data;
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
if (typeof parsed === 'object' && parsed !== null) {
const sanitized = { ...parsed };
// Remove sensitive fields from response
const sensitiveFields = ['password', 'token', 'secret', 'key'];
const sanitizeObject = (obj) => {
if (Array.isArray(obj)) {
return obj.map(item => sanitizeObject(item));
} else if (obj && typeof obj === 'object') {
const result = {};
Object.keys(obj).forEach(key => {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
result[key] = '[REDACTED]';
} else {
result[key] = sanitizeObject(obj[key]);
}
});
return result;
}
return obj;
};
return sanitizeObject(sanitized);
}
return parsed;
} catch (error) {
return '[UNPARSEABLE_RESPONSE]';
}
};
/**
* Health check endpoint logger
* Reduces noise from health check requests
*/
const healthCheckLogger = (req, res, next) => {
// Skip detailed logging for health checks
if (req.originalUrl === '/health' || req.originalUrl === '/api/health') {
return next();
}
return requestLogger(req, res, next);
};
module.exports = {
requestLogger,
healthCheckLogger,
sanitizeBody,
sanitizeResponseData
};