- Added `xmlchars` dependency to `pnpm-lock.yaml` for improved XML character handling. - Updated IAM Service audit plan to streamline post-deployment monitoring tasks. - Enhanced Dockerfile to prune development dependencies after build for a leaner production image. - Introduced a new encryption key configuration in the environment example for better security practices. - Refactored multiple service files to improve import organization and maintainability. - Improved error handling in seed scripts to provide more detailed logging on failures. - Updated various controllers and services to ensure consistent import statements and enhance readability. These changes aim to improve the overall functionality, security, and maintainability of the IAM Service.
206 lines
6.8 KiB
TypeScript
206 lines
6.8 KiB
TypeScript
import { logger } from '@goodgo/logger';
|
|
import express from 'express';
|
|
|
|
import { ErrorCode, getStatusFromErrorCode, isOperationalError } from '../errors/error-codes';
|
|
import { HttpError } from '../errors/http-error';
|
|
|
|
/**
|
|
* EN: Global error handler middleware with enhanced error handling
|
|
* VI: Middleware xử lý lỗi toàn cục với enhanced error handling
|
|
*/
|
|
export const errorHandler = (
|
|
err: any,
|
|
req: express.Request,
|
|
res: express.Response,
|
|
_next: express.NextFunction
|
|
): void => {
|
|
let statusCode = 500;
|
|
let errorCode = ErrorCode.INTERNAL_ERROR;
|
|
let message = 'Internal server error / Lỗi máy chủ nội bộ';
|
|
let details: any = undefined;
|
|
let isOperational = false;
|
|
|
|
// EN: Handle HttpError instances (our custom errors)
|
|
// VI: Xử lý HttpError instances (custom errors của chúng ta)
|
|
if (err instanceof HttpError) {
|
|
statusCode = err.statusCode;
|
|
errorCode = err.errorCode as ErrorCode;
|
|
message = err.message;
|
|
details = err.details;
|
|
isOperational = err.isOperational;
|
|
}
|
|
// EN: Handle Prisma errors
|
|
// VI: Xử lý Prisma errors
|
|
else if (err.code && typeof err.code === 'string') {
|
|
if (err.code === 'P2002') {
|
|
// Unique constraint violation
|
|
statusCode = 409;
|
|
errorCode = ErrorCode.CONSTRAINT_VIOLATION;
|
|
message = 'Resource already exists / Tài nguyên đã tồn tại';
|
|
isOperational = true;
|
|
} else if (err.code.startsWith('P1')) {
|
|
// Database connection/query errors
|
|
statusCode = 500;
|
|
errorCode = ErrorCode.DATABASE_ERROR;
|
|
message = 'Database operation failed / Thao tác database thất bại';
|
|
isOperational = false;
|
|
} else if (err.code.startsWith('P2')) {
|
|
// Data validation errors
|
|
statusCode = 422;
|
|
errorCode = ErrorCode.VALIDATION_ERROR;
|
|
message = 'Data validation failed / Validation dữ liệu thất bại';
|
|
isOperational = true;
|
|
}
|
|
}
|
|
// EN: Handle JWT errors
|
|
// VI: Xử lý JWT errors
|
|
else if (err.name === 'JsonWebTokenError') {
|
|
statusCode = 401;
|
|
errorCode = ErrorCode.INVALID_TOKEN;
|
|
message = 'Invalid authentication token / Token xác thực không hợp lệ';
|
|
isOperational = true;
|
|
} else if (err.name === 'TokenExpiredError') {
|
|
statusCode = 401;
|
|
errorCode = ErrorCode.TOKEN_EXPIRED;
|
|
message = 'Authentication token expired / Token xác thực đã hết hạn';
|
|
isOperational = true;
|
|
}
|
|
// EN: Handle Zod validation errors
|
|
// VI: Xử lý Zod validation errors
|
|
else if (err.name === 'ZodError') {
|
|
statusCode = 422;
|
|
errorCode = ErrorCode.VALIDATION_ERROR;
|
|
message = 'Validation failed / Validation thất bại';
|
|
details = err.errors.map((e: any) => ({
|
|
field: e.path.join('.'),
|
|
message: e.message,
|
|
code: e.code,
|
|
}));
|
|
isOperational = true;
|
|
}
|
|
// EN: Handle Express/Multer file upload errors
|
|
// VI: Xử lý Express/Multer file upload errors
|
|
else if (err.name === 'MulterError') {
|
|
statusCode = 400;
|
|
errorCode = ErrorCode.INVALID_FORMAT;
|
|
message = 'File upload error / Lỗi upload file';
|
|
isOperational = true;
|
|
}
|
|
// EN: Handle rate limiting errors
|
|
// VI: Xử lý rate limiting errors
|
|
else if (err.message && err.message.includes('Too many requests')) {
|
|
statusCode = 429;
|
|
errorCode = ErrorCode.RATE_LIMIT_EXCEEDED;
|
|
message = 'Rate limit exceeded / Vượt quá giới hạn tốc độ';
|
|
isOperational = true;
|
|
}
|
|
// EN: Handle generic errors
|
|
// VI: Xử lý generic errors
|
|
else {
|
|
// EN: Try to map error message patterns
|
|
// VI: Thử map error message patterns
|
|
const errorMessage = err.message?.toLowerCase() || '';
|
|
|
|
if (errorMessage.includes('not found')) {
|
|
statusCode = 404;
|
|
errorCode = ErrorCode.NOT_FOUND;
|
|
message = err.message;
|
|
isOperational = true;
|
|
} else if (errorMessage.includes('unauthorized') || errorMessage.includes('not authenticated')) {
|
|
statusCode = 401;
|
|
errorCode = ErrorCode.UNAUTHORIZED;
|
|
message = err.message;
|
|
isOperational = true;
|
|
} else if (errorMessage.includes('forbidden') || errorMessage.includes('not allowed')) {
|
|
statusCode = 403;
|
|
errorCode = ErrorCode.FORBIDDEN;
|
|
message = err.message;
|
|
isOperational = true;
|
|
} else if (errorMessage.includes('validation') || errorMessage.includes('invalid')) {
|
|
statusCode = 422;
|
|
errorCode = ErrorCode.VALIDATION_ERROR;
|
|
message = err.message;
|
|
isOperational = true;
|
|
}
|
|
}
|
|
|
|
// EN: Prepare error details for logging
|
|
// VI: Chuẩn bị chi tiết lỗi để logging
|
|
const errorDetails = {
|
|
message: err.message,
|
|
name: err.name,
|
|
code: err.code,
|
|
statusCode,
|
|
errorCode,
|
|
isOperational,
|
|
stack: err.stack,
|
|
url: req.url,
|
|
method: req.method,
|
|
userAgent: req.get('User-Agent'),
|
|
ip: req.ip,
|
|
userId: (req as any).user?.userId,
|
|
details,
|
|
};
|
|
|
|
// EN: Log error with appropriate level
|
|
// VI: Log lỗi với level phù hợp
|
|
if (!isOperational || statusCode >= 500) {
|
|
logger.error('Unhandled error occurred / Lỗi không mong muốn xảy ra', errorDetails);
|
|
} else {
|
|
logger.warn('Operational error occurred / Lỗi operational xảy ra', errorDetails);
|
|
}
|
|
|
|
// EN: Prepare response based on environment
|
|
// VI: Chuẩn bị response dựa trên environment
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
|
|
const response = {
|
|
success: false,
|
|
error: {
|
|
code: errorCode,
|
|
message: isProduction && statusCode >= 500 ? 'Internal server error / Lỗi máy chủ nội bộ' : message,
|
|
...(details && !isProduction && { details }),
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
res.status(statusCode).json(response);
|
|
};
|
|
|
|
/**
|
|
* EN: 404 Not Found handler with enhanced error details
|
|
* VI: Handler 404 Not Found với enhanced error details
|
|
*/
|
|
export const notFoundHandler = (
|
|
req: express.Request,
|
|
_res: express.Response,
|
|
next: express.NextFunction
|
|
): void => {
|
|
const error = new HttpError(
|
|
`Route ${req.originalUrl} not found / Route ${req.originalUrl} không tìm thấy`,
|
|
404,
|
|
ErrorCode.NOT_FOUND
|
|
);
|
|
next(error);
|
|
};
|
|
|
|
/**
|
|
* EN: Async error wrapper to catch promise rejections
|
|
* VI: Async error wrapper để catch promise rejections
|
|
*/
|
|
export const asyncHandler = (fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<any> | any) => {
|
|
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* EN: Create HttpError from error code
|
|
* VI: Tạo HttpError từ error code
|
|
*/
|
|
export const createHttpError = (errorCode: ErrorCode, message?: string, details?: any): HttpError => {
|
|
const statusCode = getStatusFromErrorCode(errorCode);
|
|
const isOperational = isOperationalError(errorCode);
|
|
return new HttpError(message || `${errorCode}`, statusCode, errorCode, isOperational, details);
|
|
};
|