diff --git a/server/controllers/announcementsController.js b/server/controllers/announcementsController.js index b17b8eb91..aeb7a3b9d 100755 --- a/server/controllers/announcementsController.js +++ b/server/controllers/announcementsController.js @@ -1,5 +1,4 @@ import { createAnnouncementValidation } from "../validation/joi.js"; -import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "announcementController"; @@ -10,10 +9,9 @@ const SERVICE_NAME = "announcementController"; * @class AnnouncementController */ -class AnnouncementController { - constructor(db, stringService) { - this.db = db; - this.stringService = stringService; +class AnnouncementController extends BaseController { + constructor(commonDependencies) { + super(commonDependencies); this.createAnnouncement = this.createAnnouncement.bind(this); this.getAnnouncement = this.getAnnouncement.bind(this); } diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 12d2f77f4..00fc2caf5 100755 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,3 +1,4 @@ +import BaseController from "./baseController.js"; import { registrationBodyValidation, loginValidation, @@ -10,7 +11,6 @@ import { editUserByIdBodyValidation, editSuperadminUserByIdBodyValidation, } from "../validation/joi.js"; -import { asyncHandler, createError } from "../utils/errorUtils.js"; const SERVICE_NAME = "authController"; @@ -23,26 +23,22 @@ const SERVICE_NAME = "authController"; * @class AuthController * @description Manages user authentication and authorization operations */ -class AuthController { +class AuthController extends BaseController { /** * Creates an instance of AuthController. * + * @param {Object} commonDependencies - Common dependencies injected into the controller * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.db - Database service for data operations * @param {Object} dependencies.settingsService - Service for application settings * @param {Object} dependencies.emailService - Service for email operations * @param {Object} dependencies.jobQueue - Service for job queue operations - * @param {Object} dependencies.stringService - Service for string/localization - * @param {Object} dependencies.logger - Logger service * @param {Object} dependencies.userService - User business logic service */ - constructor({ db, settingsService, emailService, jobQueue, stringService, logger, userService }) { - this.db = db; + constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) { + super(commonDependencies); this.settingsService = settingsService; this.emailService = emailService; this.jobQueue = jobQueue; - this.stringService = stringService; - this.logger = logger; this.userService = userService; } @@ -85,7 +81,7 @@ class AuthController { * "inviteToken": "abc123..." * } */ - registerUser = asyncHandler( + registerUser = this.asyncHandler( async (req, res) => { if (req.body?.email) { req.body.email = req.body.email?.toLowerCase(); @@ -121,7 +117,7 @@ class AuthController { * "password": "SecurePass123!" * } */ - loginUser = asyncHandler( + loginUser = this.asyncHandler( async (req, res) => { if (req.body?.email) { req.body.email = req.body.email?.toLowerCase(); @@ -173,7 +169,7 @@ class AuthController { * "newPassword": "NewPass123!" * } */ - editUser = asyncHandler( + editUser = this.asyncHandler( async (req, res) => { await editUserBodyValidation.validateAsync(req.body); @@ -200,7 +196,7 @@ class AuthController { * GET /auth/users/superadmin * // Response: { "data": true } or { "data": false } */ - checkSuperadminExists = asyncHandler( + checkSuperadminExists = this.asyncHandler( async (req, res) => { const superAdminExists = await this.userService.checkSuperadminExists(); return res.success({ @@ -230,7 +226,7 @@ class AuthController { * "email": "john@example.com" * } */ - requestRecovery = asyncHandler( + requestRecovery = this.asyncHandler( async (req, res) => { await recoveryValidation.validateAsync(req.body); const email = req?.body?.email; @@ -262,7 +258,7 @@ class AuthController { * "recoveryToken": "abc123..." * } */ - validateRecovery = asyncHandler( + validateRecovery = this.asyncHandler( async (req, res) => { await recoveryTokenBodyValidation.validateAsync(req.body); await this.userService.validateRecovery(req.body.recoveryToken); @@ -294,7 +290,7 @@ class AuthController { * "recoveryToken": "abc123..." * } */ - resetPassword = asyncHandler( + resetPassword = this.asyncHandler( async (req, res) => { await newPasswordValidation.validateAsync(req.body); const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); @@ -326,7 +322,7 @@ class AuthController { * DELETE /auth/user * // Requires JWT authentication */ - deleteUser = asyncHandler( + deleteUser = this.asyncHandler( async (req, res) => { await this.userService.deleteUser(req.user); return res.success({ @@ -350,7 +346,7 @@ class AuthController { * GET /auth/users * // Requires JWT authentication with admin/superadmin role */ - getAllUsers = asyncHandler( + getAllUsers = this.asyncHandler( async (req, res) => { const allUsers = await this.userService.getAllUsers(); return res.success({ @@ -381,7 +377,7 @@ class AuthController { * GET /auth/users/507f1f77bcf86cd799439011 * // Requires JWT authentication with superadmin role */ - getUserById = asyncHandler( + getUserById = this.asyncHandler( async (req, res) => { await getUserByIdParamValidation.validateAsync(req.params); const userId = req?.params?.userId; @@ -431,7 +427,7 @@ class AuthController { * } * // Requires JWT authentication with superadmin role */ - editUserById = asyncHandler( + editUserById = this.asyncHandler( async (req, res) => { const roles = req?.user?.role; if (!roles.includes("superadmin")) { diff --git a/server/controllers/baseController.js b/server/controllers/baseController.js new file mode 100644 index 000000000..a84806a24 --- /dev/null +++ b/server/controllers/baseController.js @@ -0,0 +1,83 @@ +import { AppError } from "../service/infrastructure/errorService.js"; + +export const createCommonDependencies = (serviceRegistry, dbServiceName, loggerServiceName, errorServiceName, stringServiceName) => { + return { + db: serviceRegistry.get(dbServiceName), + errorService: serviceRegistry.get(errorServiceName), + logger: serviceRegistry.get(loggerServiceName), + stringService: serviceRegistry.get(stringServiceName), + }; +}; + +class BaseController { + constructor({ db, logger, errorService, ...additionalDependencies }) { + this.db = db; + this.logger = logger; + this.errorService = errorService; + Object.assign(this, additionalDependencies); + + this.asyncHandler = (fn, serviceName, methodName) => { + return async (req, res, next) => { + try { + await fn(req, res, next); + } catch (error) { + // Handle validation errors + if (error.isJoi) { + const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName); + return next(validationError); + } + + if (error.name === "ValidationError") { + const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName); + return next(validationError); + } + + if (error.name === "CastError") { + const notFoundError = this.errorService.createNotFoundError( + "Invalid resource identifier", + { field: error.path, value: error.value }, + serviceName, + methodName + ); + return next(notFoundError); + } + + if (error.code === "11000") { + const conflictError = this.errorService.createConflictError("Resource already exists", { + originalError: error.message, + code: error.code, + }); + conflictError.service = serviceName; + conflictError.method = methodName; + return next(conflictError); + } + + if (error instanceof AppError) { + error.service = error.service || serviceName; + error.method = error.method || methodName; + return next(error); + } + + if (error.status) { + const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, { + originalError: error.message, + stack: error.stack, + }); + return next(appError); + } + + // For unknown errors, create a server error + const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", { + originalError: error.message, + stack: error.stack, + }); + appError.service = serviceName; + appError.method = methodName; + appError.stack = error.stack; // Preserve original stack + return next(appError); + } + }; + }; + } +} +export default BaseController; diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js index 304f89c6d..08b6c2de9 100755 --- a/server/controllers/checkController.js +++ b/server/controllers/checkController.js @@ -1,3 +1,4 @@ +import BaseController from "./baseController.js"; import { getChecksParamValidation, getChecksQueryValidation, @@ -9,7 +10,6 @@ import { ackAllChecksParamValidation, ackAllChecksBodyValidation, } from "../validation/joi.js"; -import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "checkController"; @@ -22,20 +22,18 @@ const SERVICE_NAME = "checkController"; * @class CheckController * @description Manages check operations and monitoring data */ -class CheckController { +class CheckController extends BaseController { /** * Creates an instance of CheckController. * + * @param {Object} commonDependencies - Common dependencies injected into the controller * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.db - Database service for data operations * @param {Object} dependencies.settingsService - Service for application settings - * @param {Object} dependencies.stringService - Service for string/localization * @param {Object} dependencies.checkService - Check business logic service */ - constructor({ db, settingsService, stringService, checkService }) { - this.db = db; + constructor(commonDependencies, { settingsService, checkService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; this.checkService = checkService; } @@ -67,7 +65,7 @@ class CheckController { * GET /checks/monitor/507f1f77bcf86cd799439011?page=1&rowsPerPage=10&status=down * // Requires JWT authentication */ - getChecksByMonitor = asyncHandler( + getChecksByMonitor = this.asyncHandler( async (req, res) => { await getChecksParamValidation.validateAsync(req.params); await getChecksQueryValidation.validateAsync(req.query); @@ -109,7 +107,7 @@ class CheckController { * GET /checks/team?page=1&rowsPerPage=20&status=down&ack=false * // Requires JWT authentication */ - getChecksByTeam = asyncHandler( + getChecksByTeam = this.asyncHandler( async (req, res) => { await getTeamChecksQueryValidation.validateAsync(req.query); const checkData = await this.checkService.getChecksByTeam({ @@ -140,7 +138,7 @@ class CheckController { * // Requires JWT authentication * // Response includes counts by status, time ranges, etc. */ - getChecksSummaryByTeamId = asyncHandler( + getChecksSummaryByTeamId = this.asyncHandler( async (req, res) => { const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); return res.success({ @@ -176,7 +174,7 @@ class CheckController { * } * // Requires JWT authentication */ - ackCheck = asyncHandler( + ackCheck = this.asyncHandler( async (req, res) => { await ackCheckBodyValidation.validateAsync(req.body); @@ -220,7 +218,7 @@ class CheckController { * } * // Requires JWT authentication */ - ackAllChecks = asyncHandler( + ackAllChecks = this.asyncHandler( async (req, res) => { await ackAllChecksParamValidation.validateAsync(req.params); await ackAllChecksBodyValidation.validateAsync(req.body); @@ -266,7 +264,7 @@ class CheckController { * // Requires JWT authentication * // Response: { "data": { "deletedCount": 150 } } */ - deleteChecks = asyncHandler( + deleteChecks = this.asyncHandler( async (req, res) => { await deleteChecksParamValidation.validateAsync(req.params); @@ -300,7 +298,7 @@ class CheckController { * // Requires JWT authentication * // Response: { "data": { "deletedCount": 1250 } } */ - deleteChecksByTeamId = asyncHandler( + deleteChecksByTeamId = this.asyncHandler( async (req, res) => { await deleteChecksByTeamIdParamValidation.validateAsync(req.params); @@ -336,7 +334,7 @@ class CheckController { * // Requires JWT authentication * // Sets check TTL to 30 days */ - updateChecksTTL = asyncHandler( + updateChecksTTL = this.asyncHandler( async (req, res) => { await updateChecksTTLBodyValidation.validateAsync(req.body); diff --git a/server/controllers/controllerUtils.js b/server/controllers/controllerUtils.js index 2b3a4198c..f21ba154b 100755 --- a/server/controllers/controllerUtils.js +++ b/server/controllers/controllerUtils.js @@ -1,12 +1,10 @@ -import { createServerError } from "../utils/errorUtils.js"; - const fetchMonitorCertificate = async (sslChecker, monitor) => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; const cert = await sslChecker(hostname); // Throw an error if no cert or if cert.validTo is not present if (cert?.validTo === null || cert?.validTo === undefined) { - throw createServerError("Certificate not found"); + throw new Error("Certificate not found"); } return cert; }; diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index 585684211..91b90f96b 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -1,6 +1,5 @@ -import { asyncHandler } from "../utils/errorUtils.js"; - const SERVICE_NAME = "diagnosticController"; +import BaseController from "./baseController.js"; /** * Diagnostic Controller * @@ -10,14 +9,15 @@ const SERVICE_NAME = "diagnosticController"; * @class DiagnosticController * @description Manages system diagnostics and performance monitoring */ -class DiagnosticController { +class DiagnosticController extends BaseController { /** * Creates an instance of DiagnosticController. - * + * @param {Object} commonDependencies - Common dependencies injected into the controller * @param {Object} dependencies - The dependencies required by the controller * @param {Object} dependencies.diagnosticService - Service for system diagnostics and monitoring */ - constructor({ diagnosticService }) { + constructor(commonDependencies, { diagnosticService }) { + super(commonDependencies); this.diagnosticService = diagnosticService; } @@ -41,7 +41,7 @@ class DiagnosticController { * // - Database connection status * // - Active processes/connections */ - getSystemStats = asyncHandler( + getSystemStats = this.asyncHandler( async (req, res) => { const diagnostics = await this.diagnosticService.getSystemStats(); return res.success({ diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js index 75e98cb68..3f237c6ec 100755 --- a/server/controllers/inviteController.js +++ b/server/controllers/inviteController.js @@ -1,21 +1,19 @@ import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js"; -import { asyncHandler } from "../utils/errorUtils.js"; - +import BaseController from "./baseController.js"; const SERVICE_NAME = "inviteController"; /** * Controller for handling user invitation operations * Manages invite token generation, email sending, and token verification */ -class InviteController { +class InviteController extends BaseController { /** * Creates a new InviteController instance - * @param {Object} dependencies - Dependencies injected into the controller - * @param {Object} dependencies.stringService - Service for internationalized strings + * @param {Object} commonDependencies - Common dependencies injected into the controller * @param {Object} dependencies.inviteService - Service for invite-related operations */ - constructor({ stringService, inviteService }) { - this.stringService = stringService; + constructor(commonDependencies, { inviteService }) { + super(commonDependencies); this.inviteService = inviteService; } @@ -28,7 +26,7 @@ class InviteController { * @param {Object} res - Express response object * @returns {Promise} Response with invite token data */ - getInviteToken = asyncHandler( + getInviteToken = this.asyncHandler( async (req, res) => { const invite = req.body; const teamId = req?.user?.teamId; @@ -54,7 +52,7 @@ class InviteController { * @param {Object} res - Express response object * @returns {Promise} Response with invite token data */ - sendInviteEmail = asyncHandler( + sendInviteEmail = this.asyncHandler( async (req, res) => { const inviteRequest = req.body; inviteRequest.teamId = req?.user?.teamId; @@ -81,7 +79,7 @@ class InviteController { * @param {Object} res - Express response object * @returns {Promise} Response with verified invite data */ - verifyInviteToken = asyncHandler( + verifyInviteToken = this.asyncHandler( async (req, res) => { await inviteVerificationBodyValidation.validateAsync(req.body); const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); diff --git a/server/controllers/logController.js b/server/controllers/logController.js index f4887ce9a..aa2780eb6 100644 --- a/server/controllers/logController.js +++ b/server/controllers/logController.js @@ -1,14 +1,13 @@ -import { asyncHandler } from "../utils/errorUtils.js"; - +import BaseController from "./baseController.js"; const SERVICE_NAME = "LogController"; -class LogController { - constructor(logger) { - this.logger = logger; +class LogController extends BaseController { + constructor(commonDependencies) { + super(commonDependencies); } - getLogs = asyncHandler( - async (req, res, next) => { + getLogs = this.asyncHandler( + async (req, res) => { const logs = await this.logger.getLogs(); res.success({ msg: "Logs fetched successfully", diff --git a/server/controllers/maintenanceWindowController.js b/server/controllers/maintenanceWindowController.js index 95349fa66..e54726ea3 100755 --- a/server/controllers/maintenanceWindowController.js +++ b/server/controllers/maintenanceWindowController.js @@ -7,25 +7,24 @@ import { getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, } from "../validation/joi.js"; -import { asyncHandler } from "../utils/errorUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "maintenanceWindowController"; -class MaintenanceWindowController { - constructor({ db, settingsService, stringService, maintenanceWindowService }) { - this.db = db; +class MaintenanceWindowController extends BaseController { + constructor(commonDependencies, { settingsService, maintenanceWindowService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; this.maintenanceWindowService = maintenanceWindowService; } - createMaintenanceWindows = asyncHandler( + createMaintenanceWindows = this.asyncHandler( async (req, res) => { await createMaintenanceWindowBodyValidation.validateAsync(req.body); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); @@ -38,13 +37,13 @@ class MaintenanceWindowController { "createMaintenanceWindows" ); - getMaintenanceWindowById = asyncHandler( + getMaintenanceWindowById = this.asyncHandler( async (req, res) => { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); const teamId = req.user.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); @@ -58,14 +57,14 @@ class MaintenanceWindowController { "getMaintenanceWindowById" ); - getMaintenanceWindowsByTeamId = asyncHandler( + getMaintenanceWindowsByTeamId = this.asyncHandler( async (req, res) => { await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); @@ -79,13 +78,13 @@ class MaintenanceWindowController { "getMaintenanceWindowsByTeamId" ); - getMaintenanceWindowsByMonitorId = asyncHandler( + getMaintenanceWindowsByMonitorId = this.asyncHandler( async (req, res) => { await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); @@ -99,13 +98,13 @@ class MaintenanceWindowController { "getMaintenanceWindowsByMonitorId" ); - deleteMaintenanceWindow = asyncHandler( + deleteMaintenanceWindow = this.asyncHandler( async (req, res) => { await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); @@ -118,14 +117,14 @@ class MaintenanceWindowController { "deleteMaintenanceWindow" ); - editMaintenanceWindow = asyncHandler( + editMaintenanceWindow = this.asyncHandler( async (req, res) => { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); const teamId = req.user.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js index 0f0dd3e76..2187df5d0 100755 --- a/server/controllers/monitorController.js +++ b/server/controllers/monitorController.js @@ -13,16 +13,15 @@ import { getHardwareDetailsByIdQueryValidation, } from "../validation/joi.js"; import sslChecker from "ssl-checker"; -import { asyncHandler } from "../utils/errorUtils.js"; import { fetchMonitorCertificate } from "./controllerUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "monitorController"; -class MonitorController { - constructor({ db, settingsService, jobQueue, stringService, emailService, monitorService }) { - this.db = db; +class MonitorController extends BaseController { + constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) { + super(commonDependencies); this.settingsService = settingsService; this.jobQueue = jobQueue; - this.stringService = stringService; this.emailService = emailService; this.monitorService = monitorService; } @@ -30,13 +29,11 @@ class MonitorController { async verifyTeamAccess(teamId, monitorId) { const monitor = await this.db.getMonitorById(monitorId); if (!monitor.teamId.equals(teamId)) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; + throw this.errorService.createAuthorizationError(); } } - getAllMonitors = asyncHandler( + getAllMonitors = this.asyncHandler( async (req, res) => { const monitors = await this.monitorService.getAllMonitors(); return res.success({ @@ -48,7 +45,7 @@ class MonitorController { "getAllMonitors" ); - getUptimeDetailsById = asyncHandler( + getUptimeDetailsById = this.asyncHandler( async (req, res) => { const monitorId = req?.params?.monitorId; const dateRange = req?.query?.dateRange; @@ -57,7 +54,7 @@ class MonitorController { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const data = await this.monitorService.getUptimeDetailsById({ @@ -75,7 +72,7 @@ class MonitorController { "getUptimeDetailsById" ); - getMonitorStatsById = asyncHandler( + getMonitorStatsById = this.asyncHandler( async (req, res) => { await getMonitorStatsByIdParamValidation.validateAsync(req.params); await getMonitorStatsByIdQueryValidation.validateAsync(req.query); @@ -85,7 +82,7 @@ class MonitorController { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const monitorStats = await this.monitorService.getMonitorStatsById({ @@ -116,7 +113,7 @@ class MonitorController { * @returns {Promise} * @throws {Error} - Throws error if monitor not found or other database errors */ - getHardwareDetailsById = asyncHandler( + getHardwareDetailsById = this.asyncHandler( async (req, res) => { await getHardwareDetailsByIdParamValidation.validateAsync(req.params); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); @@ -125,7 +122,7 @@ class MonitorController { const dateRange = req?.query?.dateRange; const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const monitor = await this.monitorService.getHardwareDetailsById({ @@ -143,8 +140,8 @@ class MonitorController { "getHardwareDetailsById" ); - getMonitorCertificate = asyncHandler( - async (req, res, next) => { + getMonitorCertificate = this.asyncHandler( + async (req, res) => { await getCertificateParamValidation.validateAsync(req.params); const { monitorId } = req.params; @@ -162,14 +159,14 @@ class MonitorController { "getMonitorCertificate" ); - getMonitorById = asyncHandler( + getMonitorById = this.asyncHandler( async (req, res) => { await getMonitorByIdParamValidation.validateAsync(req.params); await getMonitorByIdQueryValidation.validateAsync(req.query); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId }); @@ -183,7 +180,7 @@ class MonitorController { "getMonitorById" ); - createMonitor = asyncHandler( + createMonitor = this.asyncHandler( async (req, res) => { await createMonitorBodyValidation.validateAsync(req.body); @@ -201,30 +198,30 @@ class MonitorController { "createMonitor" ); - createBulkMonitors = asyncHandler( + createBulkMonitors = this.asyncHandler( async (req, res) => { if (!req.file) { - throw new Error("No file uploaded"); + throw this.errorService.createBadRequestError("No file uploaded"); } if (!req.file.mimetype.includes("csv")) { - throw new Error("File is not a CSV"); + throw this.errorService.createBadRequestError("File is not a CSV"); } if (req.file.size === 0) { - throw new Error("File is empty"); + throw this.errorService.createBadRequestError("File is empty"); } const userId = req?.user?._id; const teamId = req?.user?.teamId; if (!userId || !teamId) { - throw new Error("Missing userId or teamId"); + throw this.errorService.createBadRequestError("Missing userId or teamId"); } const fileData = req?.file?.buffer?.toString("utf-8"); if (!fileData) { - throw new Error("Cannot get file from buffer"); + throw this.errorService.createBadRequestError("Cannot get file from buffer"); } const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId }); @@ -238,13 +235,13 @@ class MonitorController { "createBulkMonitors" ); - deleteMonitor = asyncHandler( + deleteMonitor = this.asyncHandler( async (req, res) => { await getMonitorByIdParamValidation.validateAsync(req.params); const monitorId = req.params.monitorId; const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -255,11 +252,11 @@ class MonitorController { "deleteMonitor" ); - deleteAllMonitors = asyncHandler( + deleteAllMonitors = this.asyncHandler( async (req, res) => { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); @@ -270,7 +267,7 @@ class MonitorController { "deleteAllMonitors" ); - editMonitor = asyncHandler( + editMonitor = this.asyncHandler( async (req, res) => { await getMonitorByIdParamValidation.validateAsync(req.params); await editMonitorBodyValidation.validateAsync(req.body); @@ -278,7 +275,7 @@ class MonitorController { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); @@ -292,14 +289,14 @@ class MonitorController { "editMonitor" ); - pauseMonitor = asyncHandler( + pauseMonitor = this.asyncHandler( async (req, res) => { await pauseMonitorParamValidation.validateAsync(req.params); const monitorId = req.params.monitorId; const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -313,7 +310,7 @@ class MonitorController { "pauseMonitor" ); - addDemoMonitors = asyncHandler( + addDemoMonitors = this.asyncHandler( async (req, res) => { const { _id, teamId } = req.user; const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); @@ -327,11 +324,11 @@ class MonitorController { "addDemoMonitors" ); - sendTestEmail = asyncHandler( + sendTestEmail = this.asyncHandler( async (req, res) => { const { to } = req.body; if (!to || typeof to !== "string") { - throw new Error(this.stringService.errorForValidEmailAddress); + throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress); } const messageId = await this.monitorService.sendTestEmail({ to }); @@ -344,8 +341,8 @@ class MonitorController { "sendTestEmail" ); - getMonitorsByTeamId = asyncHandler( - async (req, res, next) => { + getMonitorsByTeamId = this.asyncHandler( + async (req, res) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); @@ -363,7 +360,7 @@ class MonitorController { "getMonitorsByTeamId" ); - getMonitorsAndSummaryByTeamId = asyncHandler( + getMonitorsAndSummaryByTeamId = this.asyncHandler( async (req, res) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); @@ -372,7 +369,7 @@ class MonitorController { const type = req?.query?.type; const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain }); @@ -386,7 +383,7 @@ class MonitorController { "getMonitorsAndSummaryByTeamId" ); - getMonitorsWithChecksByTeamId = asyncHandler( + getMonitorsWithChecksByTeamId = this.asyncHandler( async (req, res) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); @@ -395,7 +392,7 @@ class MonitorController { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ @@ -419,11 +416,11 @@ class MonitorController { "getMonitorsWithChecksByTeamId" ); - exportMonitorsToCSV = asyncHandler( + exportMonitorsToCSV = this.asyncHandler( async (req, res) => { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("Team ID is required"); + throw this.errorService.createBadRequestError("Team ID is required"); } const csv = await this.monitorService.exportMonitorsToCSV({ teamId }); diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index 41af21ece..6e85b97b8 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -1,27 +1,23 @@ import { createNotificationBodyValidation } from "../validation/joi.js"; -import { asyncHandler } from "../utils/errorUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "NotificationController"; -class NotificationController { - constructor({ notificationService, stringService, statusService, db }) { +class NotificationController extends BaseController { + constructor(commonDependencies, { notificationService, statusService }) { + super(commonDependencies); this.notificationService = notificationService; - this.stringService = stringService; this.statusService = statusService; - this.db = db; } - testNotification = asyncHandler( - async (req, res, next) => { + testNotification = this.asyncHandler( + async (req, res) => { const notification = req.body; const success = await this.notificationService.sendTestNotification(notification); if (!success) { - return res.error({ - msg: "Sending notification failed", - status: 400, - }); + throw this.errorService.createServerError("Sending notification failed"); } return res.success({ @@ -32,8 +28,8 @@ class NotificationController { "testNotification" ); - createNotification = asyncHandler( - async (req, res, next) => { + createNotification = this.asyncHandler( + async (req, res) => { await createNotificationBodyValidation.validateAsync(req.body, { abortEarly: false, }); @@ -42,12 +38,12 @@ class NotificationController { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("Team ID is required"); } const userId = req?.user?._id; if (!userId) { - throw new Error("No user ID in request"); + throw this.errorService.createBadRequestError("User ID is required"); } body.userId = userId; body.teamId = teamId; @@ -62,11 +58,11 @@ class NotificationController { "createNotification" ); - getNotificationsByTeamId = asyncHandler( - async (req, res, next) => { + getNotificationsByTeamId = this.asyncHandler( + async (req, res) => { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("Team ID is required"); } const notifications = await this.db.getNotificationsByTeamId(teamId); @@ -80,18 +76,16 @@ class NotificationController { "getNotificationsByTeamId" ); - deleteNotification = asyncHandler( - async (req, res, next) => { + deleteNotification = this.asyncHandler( + async (req, res) => { const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("Team ID is required"); } const notification = await this.db.getNotificationById(req.params.id); if (!notification.teamId.equals(teamId)) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; + throw this.errorService.createAuthorizationError(); } await this.db.deleteNotificationById(req.params.id); @@ -103,19 +97,17 @@ class NotificationController { "deleteNotification" ); - getNotificationById = asyncHandler( - async (req, res, next) => { + getNotificationById = this.asyncHandler( + async (req, res) => { const notification = await this.db.getNotificationById(req.params.id); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("Team ID is required"); } if (!notification.teamId.equals(teamId)) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; + throw this.errorService.createAuthorizationError(); } return res.success({ msg: "Notification fetched successfully", @@ -126,23 +118,21 @@ class NotificationController { "getNotificationById" ); - editNotification = asyncHandler( - async (req, res, next) => { + editNotification = this.asyncHandler( + async (req, res) => { await createNotificationBodyValidation.validateAsync(req.body, { abortEarly: false, }); const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("Team ID is required"); } const notification = await this.db.getNotificationById(req.params.id); if (!notification.teamId.equals(teamId)) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; + throw this.errorService.createAuthorizationError(); } const editedNotification = await this.db.editNotification(req.params.id, req.body); @@ -155,26 +145,24 @@ class NotificationController { "editNotification" ); - testAllNotifications = asyncHandler( - async (req, res, next) => { + testAllNotifications = this.asyncHandler( + async (req, res) => { const monitorId = req.body.monitorId; const teamId = req?.user?.teamId; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("Team ID is required"); } const monitor = await this.db.getMonitorById(monitorId); if (!monitor.teamId.equals(teamId)) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; + throw this.errorService.createAuthorizationError(); } const notifications = monitor.notifications; - if (notifications.length === 0) throw new Error("No notifications"); + if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications"); const result = await this.notificationService.testAllNotifications(notifications); - if (!result) throw new Error("Failed to send all notifications"); + if (!result) throw this.errorService.createServerError("Failed to send all notifications"); return res.success({ msg: "All notifications sent successfully", }); diff --git a/server/controllers/queueController.js b/server/controllers/queueController.js index faa0febd7..757350652 100755 --- a/server/controllers/queueController.js +++ b/server/controllers/queueController.js @@ -1,15 +1,14 @@ -import { asyncHandler } from "../utils/errorUtils.js"; - +import BaseController from "./baseController.js"; const SERVICE_NAME = "JobQueueController"; -class JobQueueController { - constructor(jobQueue, stringService) { +class JobQueueController extends BaseController { + constructor(commonDependencies, { jobQueue }) { + super(commonDependencies); this.jobQueue = jobQueue; - this.stringService = stringService; } - getMetrics = asyncHandler( - async (req, res, next) => { + getMetrics = this.asyncHandler( + async (req, res) => { const metrics = await this.jobQueue.getMetrics(); res.success({ msg: this.stringService.queueGetMetrics, @@ -20,8 +19,8 @@ class JobQueueController { "getMetrics" ); - getJobs = asyncHandler( - async (req, res, next) => { + getJobs = this.asyncHandler( + async (req, res) => { const jobs = await this.jobQueue.getJobs(); return res.success({ msg: this.stringService.queueGetJobs, @@ -32,8 +31,8 @@ class JobQueueController { "getJobs" ); - getAllMetrics = asyncHandler( - async (req, res, next) => { + getAllMetrics = this.asyncHandler( + async (req, res) => { const jobs = await this.jobQueue.getJobs(); const metrics = await this.jobQueue.getMetrics(); return res.success({ @@ -45,8 +44,8 @@ class JobQueueController { "getAllMetrics" ); - addJob = asyncHandler( - async (req, res, next) => { + addJob = this.asyncHandler( + async (req, res) => { await this.jobQueue.addJob(Math.random().toString(36).substring(7)); return res.success({ msg: this.stringService.queueAddJob, @@ -56,8 +55,8 @@ class JobQueueController { "addJob" ); - flushQueue = asyncHandler( - async (req, res, next) => { + flushQueue = this.asyncHandler( + async (req, res) => { const result = await this.jobQueue.flushQueues(); return res.success({ msg: this.stringService.jobQueueFlush, @@ -68,8 +67,8 @@ class JobQueueController { "flushQueue" ); - checkQueueHealth = asyncHandler( - async (req, res, next) => { + checkQueueHealth = this.asyncHandler( + async (req, res) => { const stuckQueues = await this.jobQueue.checkQueueHealth(); return res.success({ msg: this.stringService.queueHealthCheck, diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index 798fbdd5e..d97c19e88 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -1,14 +1,13 @@ import { updateAppSettingsBodyValidation } from "../validation/joi.js"; import { sendTestEmailBodyValidation } from "../validation/joi.js"; -import { asyncHandler, createServerError } from "../utils/errorUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "SettingsController"; -class SettingsController { - constructor({ db, settingsService, stringService, emailService }) { - this.db = db; +class SettingsController extends BaseController { + constructor(commonDependencies, { settingsService, emailService }) { + super(commonDependencies); this.settingsService = settingsService; - this.stringService = stringService; this.emailService = emailService; } @@ -33,8 +32,8 @@ class SettingsController { return returnSettings; }; - getAppSettings = asyncHandler( - async (req, res, next) => { + getAppSettings = this.asyncHandler( + async (req, res) => { const dbSettings = await this.settingsService.getDBSettings(); const returnSettings = this.buildAppSettings(dbSettings); @@ -47,8 +46,8 @@ class SettingsController { "getAppSettings" ); - updateAppSettings = asyncHandler( - async (req, res, next) => { + updateAppSettings = this.asyncHandler( + async (req, res) => { await updateAppSettingsBodyValidation.validateAsync(req.body); const updatedSettings = await this.db.updateAppSettings(req.body); @@ -62,14 +61,9 @@ class SettingsController { "updateAppSettings" ); - sendTestEmail = asyncHandler( - async (req, res, next) => { - try { - await sendTestEmailBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + sendTestEmail = this.asyncHandler( + async (req, res) => { + await sendTestEmailBodyValidation.validateAsync(req.body); const { to, @@ -107,7 +101,7 @@ class SettingsController { }); if (!messageId) { - throw createServerError("Failed to send test email."); + throw this.errorService.createServerError("Failed to send test email."); } return res.success({ diff --git a/server/controllers/statusPageController.js b/server/controllers/statusPageController.js index 8333e8949..09d1d8b5f 100755 --- a/server/controllers/statusPageController.js +++ b/server/controllers/statusPageController.js @@ -1,16 +1,15 @@ import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js"; -import { asyncHandler } from "../utils/errorUtils.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "statusPageController"; -class StatusPageController { - constructor(db, stringService) { - this.db = db; - this.stringService = stringService; +class StatusPageController extends BaseController { + constructor(commonDependencies) { + super(commonDependencies); } - createStatusPage = asyncHandler( - async (req, res, next) => { + createStatusPage = this.asyncHandler( + async (req, res) => { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); @@ -30,16 +29,14 @@ class StatusPageController { "createStatusPage" ); - updateStatusPage = asyncHandler( - async (req, res, next) => { + updateStatusPage = this.asyncHandler( + async (req, res) => { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); const statusPage = await this.db.updateStatusPage(req.body, req.file); if (statusPage === null) { - const error = new Error(this.stringService.statusPageNotFound); - error.status = 404; - throw error; + throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound); } return res.success({ msg: this.stringService.statusPageUpdate, @@ -50,8 +47,8 @@ class StatusPageController { "updateStatusPage" ); - getStatusPage = asyncHandler( - async (req, res, next) => { + getStatusPage = this.asyncHandler( + async (req, res) => { const statusPage = await this.db.getStatusPage(); return res.success({ msg: this.stringService.statusPageByUrl, @@ -62,8 +59,8 @@ class StatusPageController { "getStatusPage" ); - getStatusPageByUrl = asyncHandler( - async (req, res, next) => { + getStatusPageByUrl = this.asyncHandler( + async (req, res) => { await getStatusPageParamValidation.validateAsync(req.params); await getStatusPageQueryValidation.validateAsync(req.query); @@ -77,8 +74,8 @@ class StatusPageController { "getStatusPageByUrl" ); - getStatusPagesByTeamId = asyncHandler( - async (req, res, next) => { + getStatusPagesByTeamId = this.asyncHandler( + async (req, res) => { const teamId = req.user.teamId; const statusPages = await this.db.getStatusPagesByTeamId(teamId); @@ -91,8 +88,8 @@ class StatusPageController { "getStatusPagesByTeamId" ); - deleteStatusPage = asyncHandler( - async (req, res, next) => { + deleteStatusPage = this.asyncHandler( + async (req, res) => { await this.db.deleteStatusPage(req.params.url); return res.success({ msg: this.stringService.statusPageDelete, diff --git a/server/index.js b/server/index.js index 541f9ae14..b6f1dd3fe 100755 --- a/server/index.js +++ b/server/index.js @@ -3,16 +3,17 @@ import fs from "fs"; import swaggerUi from "swagger-ui-express"; import jwt from "jsonwebtoken"; import papaparse from "papaparse"; - import express from "express"; import helmet from "helmet"; import cors from "cors"; import compression from "compression"; +import { Logger } from "./utils/logger.js"; import logger from "./utils/logger.js"; import { verifyJWT } from "./middleware/verifyJWT.js"; import { handleErrors } from "./middleware/handleErrors.js"; import { responseHandler } from "./middleware/responseHandler.js"; import { fileURLToPath } from "url"; +import { createCommonDependencies } from "./controllers/baseController.js"; import AuthRoutes from "./routes/authRoute.js"; import AuthController from "./controllers/authController.js"; @@ -58,6 +59,8 @@ import PulseQueueHelper from "./service/infrastructure/PulseQueue/PulseQueueHelp import SuperSimpleQueue from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; import SuperSimpleQueueHelper from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import ErrorService from "./service/infrastructure/errorService.js"; + // Business services import UserService from "./service/business/userService.js"; import CheckService from "./service/business/checkService.js"; @@ -186,6 +189,7 @@ const startApp = async () => { }); const redisService = new RedisService({ Redis: IORedis, logger }); + const errorService = new ErrorService(); // const jobQueueHelper = new JobQueueHelper({ // redisService, @@ -240,11 +244,13 @@ const startApp = async () => { logger, stringService, jwt, + errorService, }); const checkService = new CheckService({ db, settingsService, stringService, + errorService, }); const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ @@ -252,11 +258,13 @@ const startApp = async () => { settingsService, emailService, stringService, + errorService, }); const maintenanceWindowService = new MaintenanceWindowService({ db, settingsService, stringService, + errorService, }); const monitorService = new MonitorService({ db, @@ -266,11 +274,14 @@ const startApp = async () => { emailService, papaparse, logger, + errorService, }); // Register services // ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue); // ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue); + ServiceRegistry.register(Logger.SERVICE_NAME, logger); ServiceRegistry.register(JobQueue.SERVICE_NAME, superSimpleQueue); + ServiceRegistry.register(ErrorService.SERVICE_NAME, errorService); ServiceRegistry.register(MongoDB.SERVICE_NAME, db); ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService); ServiceRegistry.register(EmailService.SERVICE_NAME, emailService); @@ -285,6 +296,7 @@ const startApp = async () => { ServiceRegistry.register(InviteService.SERVICE_NAME, inviteService); ServiceRegistry.register(MaintenanceWindowService.SERVICE_NAME, maintenanceWindowService); ServiceRegistry.register(MonitorService.SERVICE_NAME, monitorService); + ServiceRegistry.register(DiagnosticService.SERVICE_NAME, diagnosticService); await translationService.initialize(); @@ -298,66 +310,63 @@ const startApp = async () => { process.on("SIGTERM", shutdown); //Create controllers - const authController = new AuthController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + + const commonDependencies = createCommonDependencies( + ServiceRegistry, + MongoDB.SERVICE_NAME, + Logger.SERVICE_NAME, + ErrorService.SERVICE_NAME, + StringService.SERVICE_NAME + ); + + const authController = new AuthController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), - logger: logger, userService: ServiceRegistry.get(UserService.SERVICE_NAME), }); - const monitorController = new MonitorController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const monitorController = new MonitorController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), monitorService: ServiceRegistry.get(MonitorService.SERVICE_NAME), }); - const settingsController = new SettingsController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const settingsController = new SettingsController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), }); - const checkController = new CheckController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const checkController = new CheckController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), checkService: ServiceRegistry.get(CheckService.SERVICE_NAME), }); - const inviteController = new InviteController({ - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), - inviteService, + const inviteController = new InviteController(commonDependencies, { + inviteService: ServiceRegistry.get(InviteService.SERVICE_NAME), }); - const maintenanceWindowController = new MaintenanceWindowController({ - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), maintenanceWindowService: ServiceRegistry.get(MaintenanceWindowService.SERVICE_NAME), }); - const queueController = new QueueController(ServiceRegistry.get(JobQueue.SERVICE_NAME), ServiceRegistry.get(StringService.SERVICE_NAME)); - - const logController = new LogController(logger); - - const statusPageController = new StatusPageController(ServiceRegistry.get(MongoDB.SERVICE_NAME), ServiceRegistry.get(StringService.SERVICE_NAME)); - - const notificationController = new NotificationController({ - notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME), - stringService: ServiceRegistry.get(StringService.SERVICE_NAME), - statusService: ServiceRegistry.get(StatusService.SERVICE_NAME), - db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + const queueController = new QueueController(commonDependencies, { + jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), }); - const diagnosticController = new DiagnosticController({ - diagnosticService, + const logController = new LogController(commonDependencies); + + const statusPageController = new StatusPageController(commonDependencies); + + const notificationController = new NotificationController(commonDependencies, { + notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME), + statusService: ServiceRegistry.get(StatusService.SERVICE_NAME), + }); + + const diagnosticController = new DiagnosticController(commonDependencies, { + diagnosticService: ServiceRegistry.get(DiagnosticService.SERVICE_NAME), }); //Create routes @@ -370,9 +379,9 @@ const startApp = async () => { const queueRoutes = new QueueRoutes(queueController); const logRoutes = new LogRoutes(logController); const statusPageRoutes = new StatusPageRoutes(statusPageController); - const notificationRoutes = new NotificationRoutes(notificationController); const diagnosticRoutes = new DiagnosticRoutes(diagnosticController); + // Middleware app.use(express.static(frontendPath)); app.use(responseHandler); @@ -415,16 +424,16 @@ const startApp = async () => { //routes app.use("/api/v1/auth", authRoutes.getRouter()); - app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter()); - app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); - app.use("/api/v1/invite", inviteRoutes.getRouter()); - app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter()); - app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter()); app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter()); - app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); - app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter()); + app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter()); + app.use("/api/v1/invite", inviteRoutes.getRouter()); + app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter()); + app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); + app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter()); app.use("/api/v1/status-page", statusPageRoutes.getRouter()); + app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); + app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); app.use("/api/v1/health", (req, res) => { res.json({ diff --git a/server/middleware/handleErrors.js b/server/middleware/handleErrors.js index 1efce6f67..782d37ad9 100755 --- a/server/middleware/handleErrors.js +++ b/server/middleware/handleErrors.js @@ -3,7 +3,6 @@ import ServiceRegistry from "../service/system/serviceRegistry.js"; import StringService from "../service/system/stringService.js"; const handleErrors = (error, req, res, next) => { - console.log("ERROR", error); const status = error.status || 500; const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); const message = error.message || stringService.authIncorrectPassword; diff --git a/server/service/business/checkService.js b/server/service/business/checkService.js index e52a4919a..3d8206644 100644 --- a/server/service/business/checkService.js +++ b/server/service/business/checkService.js @@ -3,37 +3,30 @@ const SERVICE_NAME = "checkService"; class CheckService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, stringService }) { + constructor({ db, settingsService, stringService, errorService }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; + this.errorService = errorService; } getChecksByMonitor = async ({ monitorId, query, teamId }) => { if (!monitorId) { - throw new Error("No monitor ID in request"); + throw this.errorService.createBadRequestError("No monitor ID in request"); } if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } const monitor = await this.db.getMonitorById(monitorId); if (!monitor) { - const err = new Error("Monitor not found"); - err.status = 404; - err.service = SERVICE_NAME; - err.method = "getChecksByMonitor"; - throw err; + throw this.errorService.createNotFoundError("Monitor not found"); } if (!monitor.teamId.equals(teamId)) { - const err = new Error("Unauthorized"); - err.status = 403; - err.service = SERVICE_NAME; - err.method = "getChecksByMonitor"; - throw err; + throw this.errorService.createAuthorizationError(); } let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; @@ -55,7 +48,7 @@ class CheckService { let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } const checkData = await this.db.getChecksByTeam({ @@ -72,7 +65,7 @@ class CheckService { getChecksSummaryByTeamId = async ({ teamId }) => { if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } const summary = await this.db.getChecksSummaryByTeamId({ teamId }); @@ -81,11 +74,11 @@ class CheckService { ackCheck = async ({ checkId, teamId, ack }) => { if (!checkId) { - throw new Error("No check ID in request"); + throw this.errorService.createBadRequestError("No check ID in request"); } if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } const updatedCheck = await this.db.ackCheck(checkId, teamId, ack); @@ -95,20 +88,16 @@ class CheckService { ackAllChecks = async ({ monitorId, path, teamId, ack }) => { if (path === "monitor") { if (!monitorId) { - throw new Error("No monitor ID in request"); + throw this.errorService.createBadRequestError("No monitor ID in request"); } const monitor = await this.db.getMonitorById(monitorId); if (!monitor) { - throw new Error("Monitor not found"); + throw this.errorService.createNotFoundError("Monitor not found"); } if (!monitor.teamId.equals(teamId)) { - const err = new Error("Unauthorized"); - err.status = 403; - err.service = SERVICE_NAME; - err.method = "ackAllChecks"; - throw err; + throw this.errorService.createAuthorizationError(); } } @@ -118,29 +107,21 @@ class CheckService { deleteChecks = async ({ monitorId, teamId }) => { if (!monitorId) { - throw new Error("No monitor ID in request"); + throw this.errorService.createBadRequestError("No monitor ID in request"); } if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } const monitor = await this.db.getMonitorById(monitorId); if (!monitor) { - const err = new Error("Monitor not found"); - err.status = 404; - err.service = SERVICE_NAME; - err.method = "deleteChecks"; - throw err; + throw this.errorService.createNotFoundError("Monitor not found"); } if (!monitor.teamId.equals(teamId)) { - const err = new Error("Unauthorized"); - err.status = 403; - err.service = SERVICE_NAME; - err.method = "deleteChecks"; - throw err; + throw this.errorService.createAuthorizationError(); } const deletedCount = await this.db.deleteChecks(monitorId); @@ -148,7 +129,7 @@ class CheckService { }; deleteChecksByTeamId = async ({ teamId }) => { if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } const deletedCount = await this.db.deleteChecksByTeamId(teamId); diff --git a/server/service/business/inviteService.js b/server/service/business/inviteService.js index b70c151d4..d23d725e4 100644 --- a/server/service/business/inviteService.js +++ b/server/service/business/inviteService.js @@ -1,14 +1,14 @@ const SERVICE_NAME = "inviteService"; -import { createServerError } from "../../utils/errorUtils.js"; class InviteService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, emailService, stringService }) { + constructor({ db, settingsService, emailService, stringService, errorService }) { this.db = db; this.settingsService = settingsService; this.emailService = emailService; this.stringService = stringService; + this.errorService = errorService; } getInviteToken = async ({ invite, teamId }) => { @@ -27,7 +27,7 @@ class InviteService { }); const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html); if (!result) { - throw createServerError("Failed to send invite e-mail... Please verify your settings."); + throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); } }; diff --git a/server/service/business/maintenanceWindowService.js b/server/service/business/maintenanceWindowService.js index 6140a5682..0f58aca85 100644 --- a/server/service/business/maintenanceWindowService.js +++ b/server/service/business/maintenanceWindowService.js @@ -2,10 +2,11 @@ const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, stringService }) { + constructor({ db, settingsService, stringService, errorService }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; + this.errorService = errorService; } createMaintenanceWindow = async ({ teamId, body }) => { @@ -15,11 +16,7 @@ class MaintenanceWindowService { const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId)); if (unauthorizedMonitors.length > 0) { - const error = new Error("Unauthorized access to one or more monitors"); - error.status = 403; - error.service = SERVICE_NAME; - error.method = "createMaintenanceWindows"; - throw error; + throw this.errorService.createAuthorizationError(); } const dbTransactions = monitorIds.map((monitorId) => { diff --git a/server/service/business/monitorService.js b/server/service/business/monitorService.js index d0e8f92ca..2a2b27aa3 100644 --- a/server/service/business/monitorService.js +++ b/server/service/business/monitorService.js @@ -1,12 +1,10 @@ import { createMonitorsBodyValidation } from "../../validation/joi.js"; -import { createServerError } from "../../utils/errorUtils.js"; -import logger from "../../utils/logger.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse }) { + constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService }) { this.db = db; this.settingsService = settingsService; this.jobQueue = jobQueue; @@ -14,14 +12,13 @@ class MonitorService { this.emailService = emailService; this.papaparse = papaparse; this.logger = logger; + this.errorService = errorService; } verifyTeamAccess = async ({ teamId, monitorId }) => { const monitor = await this.db.getMonitorById(monitorId); if (!monitor?.teamId?.equals(teamId)) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; + throw this.errorService.createAuthorizationError(); } }; @@ -93,7 +90,7 @@ class MonitorService { if (["port", "interval"].includes(header)) { const num = parseInt(value, 10); if (isNaN(num)) { - throw new Error(`${header} should be a valid number, got: ${value}`); + throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`); } return num; } @@ -103,11 +100,11 @@ class MonitorService { complete: async ({ data, errors }) => { try { if (errors.length > 0) { - throw createServerError("Error parsing CSV"); + throw this.errorService.createServerError("Error parsing CSV"); } if (!data || data.length === 0) { - throw createServerError("CSV file contains no data rows"); + throw this.errorService.createServerError("CSV file contains no data rows"); } const enrichedData = data.map((monitor) => ({ @@ -156,7 +153,7 @@ class MonitorService { await this.db.deletePageSpeedChecksByMonitorId(monitor._id); await this.db.deleteNotificationsByMonitorId(monitor._id); } catch (error) { - logger.warn({ + this.logger.warn({ message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, service: SERVICE_NAME, method: "deleteAllMonitors", @@ -195,7 +192,7 @@ class MonitorService { const messageId = await this.emailService.sendEmail(to, subject, html); if (!messageId) { - throw createServerError("Failed to send test email."); + throw this.errorService.createServerError("Failed to send test email."); } return messageId; @@ -240,10 +237,10 @@ class MonitorService { }; exportMonitorsToCSV = async ({ teamId }) => { - const monitors = await this.monitorService.getMonitorsByTeamId({ teamId }); + const monitors = await this.db.getMonitorsByTeamId({ teamId }); if (!monitors || monitors.length === 0) { - throw new Error("No monitors to export"); + throw this.errorService.createNotFoundError("No monitors to export"); } const csvData = monitors?.filteredMonitors?.map((monitor) => ({ diff --git a/server/service/business/userService.js b/server/service/business/userService.js index 90906552d..12dd4207f 100644 --- a/server/service/business/userService.js +++ b/server/service/business/userService.js @@ -1,15 +1,15 @@ const SERVICE_NAME = "userService"; -import { createAuthError, createError } from "../../utils/errorUtils.js"; class UserService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, emailService, settingsService, logger, stringService, jwt }) { + constructor({ db, emailService, settingsService, logger, stringService, jwt, errorService }) { this.db = db; this.emailService = emailService; this.settingsService = settingsService; this.logger = logger; this.stringService = stringService; this.jwt = jwt; + this.errorService = errorService; } issueToken = (payload, appSettings) => { @@ -80,7 +80,7 @@ class UserService { // Compare password const match = await user.comparePassword(password); if (match !== true) { - throw createAuthError(this.stringService.authIncorrectPassword); + throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword); } // Remove password from user object. Should this be abstracted to DB layer? @@ -109,7 +109,7 @@ class UserService { // If not a match, throw a 403 // 403 instead of 401 to avoid triggering axios interceptor if (!match) { - throw createError(this.stringService.authIncorrectPassword, 403); + throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword); } // If a match, update the password updates.password = updates.newPassword; @@ -154,23 +154,23 @@ class UserService { deleteUser = async (user) => { const email = user?.email; if (!email) { - throw new Error("No email in request"); + throw this.errorService.createBadRequestError("No email in request"); } const teamId = user?.teamId; const userId = user?._id; if (!teamId) { - throw new Error("No team ID in request"); + throw this.errorService.createBadRequestError("No team ID in request"); } if (!userId) { - throw new Error("No user ID in request"); + throw this.errorService.createBadRequestError("No user ID in request"); } const roles = user?.role; if (roles.includes("demo")) { - throw new Error("Demo user cannot be deleted"); + throw this.errorService.createBadRequestError("Demo user cannot be deleted"); } // 1. Find all the monitors associated with the team ID if superadmin diff --git a/server/service/infrastructure/errorService.js b/server/service/infrastructure/errorService.js new file mode 100644 index 000000000..552816b73 --- /dev/null +++ b/server/service/infrastructure/errorService.js @@ -0,0 +1,98 @@ +export class AppError extends Error { + constructor(message, status = 500, service = null, method = null, details = null) { + super(message); + this.status = status; + this.service = service; + this.method = method; + this.details = details; + + Error.captureStackTrace(this, this.constructor); + } +} + +class ValidationError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 422, service, method, details); + } +} + +class AuthenticationError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 401, service, method, details); + } +} + +class AuthorizationError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 403, service, method, details); + } +} + +class NotFoundError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 404, service, method, details); + } +} + +class ConflictError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 409, service, method, details); + } +} + +class DatabaseError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 500, service, method, details); + } +} + +class BadRequestError extends AppError { + constructor(message, details = null, service = null, method = null) { + super(message, 400, service, method, details); + } +} + +const SERVICE_NAME = "ErrorService"; +class ErrorService { + static SERVICE_NAME = SERVICE_NAME; + + constructor() {} + + createError = (message, status = 500, service = null, method = null, details = null) => { + return new AppError(message, status, service, method, details); + }; + + createValidationError = (message, details = null, service = null, method = null) => { + return new ValidationError(message, details, service, method); + }; + + createAuthenticationError = (message = "Unauthorized", details = null, service = null, method = null) => { + return new AuthenticationError(message, details, service, method); + }; + + createAuthorizationError = (message, details = null, service = null, method = null) => { + return new AuthorizationError(message, details, service, method); + }; + + createNotFoundError = (message, details = null, service = null, method = null) => { + return new NotFoundError(message, details, service, method); + }; + + createConflictError = (message, details = null, service = null, method = null) => { + return new ConflictError(message, details, service, method); + }; + + createDatabaseError = (message, details = null, service = null, method = null) => { + return new DatabaseError(message, details, service, method); + }; + + createServerError = (message, details = null, service = null, method = null) => { + return this.createError(message, 500, service, method, details); + }; + + createBadRequestError = (message = "BadRequest", details = null, service = null, method = null) => { + return new BadRequestError(message, details, service, method); + }; +} + +export default ErrorService; diff --git a/server/utils/errorUtils.js b/server/utils/errorUtils.js deleted file mode 100644 index 7d9a2959d..000000000 --- a/server/utils/errorUtils.js +++ /dev/null @@ -1,81 +0,0 @@ -class AppError extends Error { - constructor(message, status = 500, service = null, method = null, details = null) { - super(message); - this.status = status; - this.service = service; - this.method = method; - this.details = details; - - Error.captureStackTrace(this, this.constructor); - } -} - -export const createError = (message, status = 500, service = null, method = null, details = null) => { - return new AppError(message, status, service, method, details); -}; - -export const createValidationError = (message, details = null, service = null, method = null) => { - return createError(message, 422, service, method, details); -}; - -export const createAuthError = (message, details = null, service = null, method = null) => { - return createError(message, 401, service, method, details); -}; - -export const createForbiddenError = (message, details = null, service = null, method = null) => { - return createError(message, 403, service, method, details); -}; - -export const createNotFoundError = (message, details = null, service = null, method = null) => { - return createError(message, 404, service, method, details); -}; - -export const createConflictError = (message, details = null, service = null, method = null) => { - return createError(message, 409, service, method, details); -}; - -export const createServerError = (message, details = null, service = null, method = null) => { - return createError(message, 500, service, method, details); -}; - -export const asyncHandler = (fn, serviceName, methodName) => { - return async (req, res, next) => { - try { - await fn(req, res, next); - } catch (error) { - // Handle validation errors - if (error.isJoi || error.name === "ValidationError") { - const validationError = createValidationError(error.message, error.details, serviceName, methodName); - return next(validationError); - } - - if (error instanceof AppError) { - error.service = error.service || serviceName; - error.method = error.method || methodName; - return next(error); - } - - if (error.code === "23505") { - const appError = createConflictError("Resource already exists", { - originalError: error.message, - code: error.code, - }); - appError.service = serviceName; - appError.method = methodName; - return next(appError); - } - - if (error.status) { - const appError = createError(error.message, error.status, serviceName, methodName, { originalError: error.message, stack: error.stack }); - return next(appError); - } - - // For unknown errors, create a server error - const appError = createServerError(error.message || "An unexpected error occurred", { originalError: error.message, stack: error.stack }); - appError.service = serviceName; - appError.method = methodName; - appError.stack = error.stack; // Preserve original stack - return next(appError); - } - }; -}; diff --git a/server/utils/logger.js b/server/utils/logger.js index 6fc240e4c..8cb57f9a7 100755 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -2,7 +2,10 @@ import { createLogger, format, transports } from "winston"; import dotenv from "dotenv"; dotenv.config(); +const SERVICE_NAME = "Logger"; + class Logger { + static SERVICE_NAME = SERVICE_NAME; constructor() { this.logCache = []; this.maxCacheSize = 1000;