diff --git a/server/controllers/announcementsController.js b/server/controllers/announcementsController.js index 024ed95a5..b17b8eb91 100755 --- a/server/controllers/announcementsController.js +++ b/server/controllers/announcementsController.js @@ -1,5 +1,5 @@ import { createAnnouncementValidation } from "../validation/joi.js"; -import { handleError } from "./controllerUtils.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "announcementController"; @@ -28,16 +28,10 @@ class AnnouncementController { * * @returns {Promise} A promise that resolves once the response is sent. */ - createAnnouncement = async (req, res, next) => { - try { + createAnnouncement = asyncHandler( + async (req, res, next) => { await createAnnouncementValidation.validateAsync(req.body); - } catch (error) { - return next(handleError(error, SERVICE_NAME)); // Handle Joi validation errors - } - - const { title, message } = req.body; - - try { + const { title, message } = req.body; const announcementData = { title: title.trim(), message: message.trim(), @@ -49,10 +43,10 @@ class AnnouncementController { msg: this.stringService.createAnnouncement, data: newAnnouncement, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createAnnouncement")); - } - }; + }, + SERVICE_NAME, + "createAnnouncement" + ); /** * Handles retrieving announcements with pagination. @@ -63,17 +57,17 @@ class AnnouncementController { * - `msg`: A message about the success of the request. * @param {Function} next - The next middleware function in the stack for error handling. */ - getAnnouncement = async (req, res, next) => { - try { + getAnnouncement = asyncHandler( + async (req, res, next) => { const allAnnouncements = await this.db.getAnnouncements(); return res.success({ msg: this.stringService.getAnnouncement, data: allAnnouncements, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAnnouncement")); - } - }; + }, + SERVICE_NAME, + "getAnnouncement" + ); } export default AnnouncementController; diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 1d8c2af0e..e4a682fec 100755 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -10,7 +10,8 @@ import { import jwt from "jsonwebtoken"; import { getTokenFromHeaders } from "../utils/utils.js"; import crypto from "crypto"; -import { handleValidationError, handleError } from "./controllerUtils.js"; +import { asyncHandler, createAuthError, createError } from "../utils/errorUtils.js"; + const SERVICE_NAME = "authController"; class AuthController { @@ -32,15 +33,10 @@ class AuthController { * @throws {Error} */ issueToken = (payload, appSettings) => { - try { - const tokenTTL = appSettings?.jwtTTL ?? "2h"; - const tokenSecret = appSettings?.jwtSecret; - const payloadData = payload; - - return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); - } catch (error) { - throw handleError(error, SERVICE_NAME, "issueToken"); - } + const tokenTTL = appSettings?.jwtTTL ?? "2h"; + const tokenSecret = appSettings?.jwtSecret; + const payloadData = payload; + return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; /** @@ -55,19 +51,14 @@ class AuthController { * @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - registerUser = async (req, res, next) => { - try { + registerUser = asyncHandler( + async (req, res, next) => { if (req.body?.email) { req.body.email = req.body.email?.toLowerCase(); } await registrationBodyValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - // Create a new user - try { + + // Create a new user const user = req.body; // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.db.checkSuperadmin(req, res); @@ -123,10 +114,10 @@ class AuthController { msg: this.stringService.authCreateUser, data: { user: newUser, token: token }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "registerController")); - } - }; + }, + SERVICE_NAME, + "registerUser" + ); /** * Logs in a user by validating the user's credentials and issuing a JWT token. @@ -140,18 +131,13 @@ class AuthController { * @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token. * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect. */ - loginUser = async (req, res, next) => { - try { + loginUser = asyncHandler( + async (req, res, next) => { if (req.body?.email) { req.body.email = req.body.email?.toLowerCase(); } await loginValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - try { + const { email, password } = req.body; // Check if user exists @@ -160,10 +146,7 @@ class AuthController { // Compare password const match = await user.comparePassword(password); if (match !== true) { - const error = new Error(this.stringService.authIncorrectPassword); - error.status = 401; - next(error); - return; + throw createAuthError(this.stringService.authIncorrectPassword); } // Remove password from user object. Should this be abstracted to DB layer? @@ -184,11 +167,10 @@ class AuthController { token: token, }, }); - } catch (error) { - error.status = 401; - next(handleError(error, SERVICE_NAME, "loginUser")); - } - }; + }, + SERVICE_NAME, + "loginUser" + ); /** * Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password. @@ -204,26 +186,16 @@ class AuthController { * @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data. * @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403). */ - editUser = async (req, res, next) => { - try { + editUser = asyncHandler( + async (req, res, next) => { await editUserParamValidation.validateAsync(req.params); await editUserBodyValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - // TODO is this neccessary any longer? Verify ownership middleware should handle this - if (req.params.userId !== req.user._id.toString()) { - const error = new Error(this.stringService.unauthorized); - error.status = 401; - error.service = SERVICE_NAME; - next(error); - return; - } + // TODO is this neccessary any longer? Verify ownership middleware should handle this + if (req.params.userId !== req.user._id.toString()) { + throw createAuthError(this.stringService.unauthorized); + } - try { // Change Password check if (req.body.password && req.body.newPassword) { // Get token from headers @@ -240,10 +212,7 @@ class AuthController { // If not a match, throw a 403 // 403 instead of 401 to avoid triggering axios interceptor if (!match) { - const error = new Error(this.stringService.authIncorrectPassword); - error.status = 403; - next(error); - return; + throw createError(this.stringService.authIncorrectPassword, 403); } // If a match, update the password req.body.password = req.body.newPassword; @@ -254,10 +223,10 @@ class AuthController { msg: this.stringService.authUpdateUser, data: updatedUser, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "userEditController")); - } - }; + }, + SERVICE_NAME, + "editUser" + ); /** * Checks if a superadmin account exists in the database. @@ -268,18 +237,17 @@ class AuthController { * @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin. * @throws {Error} If there is an error during the process. */ - checkSuperadminExists = async (req, res, next) => { - try { + checkSuperadminExists = asyncHandler( + async (req, res, next) => { const superAdminExists = await this.db.checkSuperadmin(req, res); - return res.success({ msg: this.stringService.authAdminExists, data: superAdminExists, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkSuperadminController")); - } - }; + }, + SERVICE_NAME, + "checkSuperadminExists" + ); /** * Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email. * @async @@ -291,16 +259,9 @@ class AuthController { * @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - requestRecovery = async (req, res, next) => { - try { + requestRecovery = asyncHandler( + async (req, res, next) => { await recoveryValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - try { const { email } = req.body; const user = await this.db.getUserByEmail(email); const recoveryToken = await this.db.requestRecoveryToken(req, res); @@ -323,10 +284,10 @@ class AuthController { msg: this.stringService.authCreateRecoveryToken, data: msgId, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "recoveryRequestController")); - } - }; + }, + SERVICE_NAME, + "requestRecovery" + ); /** * Validates a recovery token. The recovery token is validated and if valid, a success message is returned. * @async @@ -338,25 +299,17 @@ class AuthController { * @returns {Object} The response object with a success status and a message indicating the validation of the recovery token. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - validateRecovery = async (req, res, next) => { - try { + validateRecovery = asyncHandler( + async (req, res, next) => { await recoveryTokenValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - - try { await this.db.validateRecoveryToken(req, res); - return res.success({ msg: this.stringService.authVerifyRecoveryToken, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController")); - } - }; + }, + SERVICE_NAME, + "validateRecovery" + ); /** * Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued. @@ -370,27 +323,20 @@ class AuthController { * @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - resetPassword = async (req, res, next) => { - try { + resetPassword = asyncHandler( + async (req, res, next) => { await newPasswordValidation.validateAsync(req.body); - } catch (error) { - const validationError = handleValidationError(error, SERVICE_NAME); - next(validationError); - return; - } - try { const user = await this.db.resetPassword(req, res); const appSettings = await this.settingsService.getSettings(); const token = this.issueToken(user._doc, appSettings); - return res.success({ msg: this.stringService.authResetPassword, data: { user, token }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "resetPasswordController")); - } - }; + }, + SERVICE_NAME, + "resetPassword" + ); /** * Deletes a user and all associated monitors, checks, and alerts. @@ -401,8 +347,8 @@ class AuthController { * @returns {Object} The response object with success status and message. * @throws {Error} If user validation fails or user is not found in the database. */ - deleteUser = async (req, res, next) => { - try { + deleteUser = asyncHandler( + async (req, res, next) => { const token = getTokenFromHeaders(req.headers); const decodedToken = jwt.decode(token); const { email } = decodedToken; @@ -430,23 +376,22 @@ class AuthController { return res.success({ msg: this.stringService.authDeleteUser, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteUserController")); - } - }; + }, + SERVICE_NAME, + "deleteUser" + ); - getAllUsers = async (req, res, next) => { - try { + getAllUsers = asyncHandler( + async (req, res, next) => { const allUsers = await this.db.getAllUsers(req, res); - return res.success({ msg: this.stringService.authGetAllUsers, data: allUsers, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllUsersController")); - } - }; + }, + SERVICE_NAME, + "getAllUsers" + ); } export default AuthController; diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js index e50d5b7fc..2968614e2 100755 --- a/server/controllers/checkController.js +++ b/server/controllers/checkController.js @@ -12,9 +12,7 @@ import { ackAllChecksParamValidation, ackAllChecksBodyValidation, } from "../validation/joi.js"; -import jwt from "jsonwebtoken"; -import { getTokenFromHeaders } from "../utils/utils.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "checkController"; @@ -25,16 +23,11 @@ class CheckController { this.stringService = stringService; } - createCheck = async (req, res, next) => { - try { + createCheck = asyncHandler( + async (req, res, next) => { await createCheckParamValidation.validateAsync(req.params); await createCheckBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const checkData = { ...req.body }; const check = await this.db.createCheck(checkData); @@ -42,21 +35,16 @@ class CheckController { msg: this.stringService.checkCreate, data: check, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createCheck")); - } - }; + }, + SERVICE_NAME, + "createCheck" + ); - getChecksByMonitor = async (req, res, next) => { - try { + getChecksByMonitor = asyncHandler( + async (req, res, next) => { await getChecksParamValidation.validateAsync(req.params); await getChecksQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { monitorId } = req.params; let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = req.query; @@ -76,20 +64,16 @@ class CheckController { msg: this.stringService.checkGet, data: result, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecks")); - } - }; + }, + SERVICE_NAME, + "getChecksByMonitor" + ); - getChecksByTeam = async (req, res, next) => { - try { + getChecksByTeam = asyncHandler( + async (req, res, next) => { await getTeamChecksParamValidation.validateAsync(req.params); await getTeamChecksQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { + let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = req.query; const { teamId } = req.user; @@ -106,33 +90,28 @@ class CheckController { msg: this.stringService.checkGet, data: checkData, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getTeamChecks")); - } - }; + }, + SERVICE_NAME, + "getChecksByTeam" + ); - getChecksSummaryByTeamId = async (req, res, next) => { - try { + getChecksSummaryByTeamId = asyncHandler( + async (req, res, next) => { const { teamId } = req.user; const summary = await this.db.getChecksSummaryByTeamId({ teamId }); return res.success({ msg: this.stringService.checkGetSummary, data: summary, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId")); - } - }; + }, + SERVICE_NAME, + "getChecksSummaryByTeamId" + ); - ackCheck = async (req, res, next) => { - try { + ackCheck = asyncHandler( + async (req, res, next) => { await ackCheckBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { checkId } = req.params; const { ack } = req.body; const { teamId } = req.user; @@ -143,21 +122,16 @@ class CheckController { msg: this.stringService.checkUpdateStatus, data: updatedCheck, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "ackCheck")); - } - }; + }, + SERVICE_NAME, + "ackCheck" + ); - ackAllChecks = async (req, res, next) => { - try { + ackAllChecks = asyncHandler( + async (req, res, next) => { await ackAllChecksParamValidation.validateAsync(req.params); await ackAllChecksBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { monitorId, path } = req.params; const { ack } = req.body; const { teamId } = req.user; @@ -168,40 +142,30 @@ class CheckController { msg: this.stringService.checkUpdateStatus, data: updatedChecks, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "ackAllChecks")); - } - }; + }, + SERVICE_NAME, + "ackAllChecks" + ); - deleteChecks = async (req, res, next) => { - try { + deleteChecks = asyncHandler( + async (req, res, next) => { await deleteChecksParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const deletedCount = await this.db.deleteChecks(req.params.monitorId); return res.success({ msg: this.stringService.checkDelete, data: { deletedCount }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecks")); - } - }; + }, + SERVICE_NAME, + "deleteChecks" + ); - deleteChecksByTeamId = async (req, res, next) => { - try { + deleteChecksByTeamId = asyncHandler( + async (req, res, next) => { await deleteChecksByTeamIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { teamId } = req.user; const deletedCount = await this.db.deleteChecksByTeamId(teamId); @@ -209,23 +173,17 @@ class CheckController { msg: this.stringService.checkDelete, data: { deletedCount }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId")); - } - }; + }, + SERVICE_NAME, + "deleteChecksByTeamId" + ); - updateChecksTTL = async (req, res, next) => { - const SECONDS_PER_DAY = 86400; + updateChecksTTL = asyncHandler( + async (req, res, next) => { + const SECONDS_PER_DAY = 86400; - try { await updateChecksTTLBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { - // Get user's teamId const { teamId } = req.user; const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY; await this.db.updateChecksTTL(teamId, ttl); @@ -233,9 +191,9 @@ class CheckController { return res.success({ msg: this.stringService.checkUpdateTTL, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateTTL")); - } - }; + }, + SERVICE_NAME, + "updateChecksTtl" + ); } export default CheckController; diff --git a/server/controllers/controllerUtils.js b/server/controllers/controllerUtils.js index ec7f1122f..2b3a4198c 100755 --- a/server/controllers/controllerUtils.js +++ b/server/controllers/controllerUtils.js @@ -1,16 +1,4 @@ -const handleValidationError = (error, serviceName) => { - error.status = 422; - error.service = serviceName; - error.message = error.details?.[0]?.message || error.message || "Validation Error"; - return error; -}; - -const handleError = (error, serviceName, method, status = 500) => { - error.status === undefined ? (error.status = status) : null; - error.service === undefined ? (error.service = serviceName) : null; - error.method === undefined ? (error.method = method) : null; - return error; -}; +import { createServerError } from "../utils/errorUtils.js"; const fetchMonitorCertificate = async (sslChecker, monitor) => { const monitorUrl = new URL(monitor.url); @@ -18,9 +6,9 @@ const fetchMonitorCertificate = async (sslChecker, monitor) => { 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 new Error("Certificate not found"); + throw createServerError("Certificate not found"); } return cert; }; -export { handleValidationError, handleError, fetchMonitorCertificate }; +export { fetchMonitorCertificate }; diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index e5368866a..a350a452b 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -1,6 +1,6 @@ -import { handleError } from "./controllerUtils.js"; import v8 from "v8"; import os from "os"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "diagnosticController"; @@ -17,20 +17,20 @@ class DiagnosticController { this.getDbStats = this.getDbStats.bind(this); } - async getMonitorsByTeamIdExecutionStats(req, res, next) { - try { + getMonitorsByTeamIdExecutionStats = asyncHandler( + async (req, res, next) => { const data = await this.db.getMonitorsByTeamIdExecutionStats(req); return res.success({ msg: "OK", data, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsByTeamIdExecutionStats")); - } - } + }, + SERVICE_NAME, + "getMonitorsByTeamIdExecutionStats" + ); - async getDbStats(req, res, next) { - try { + getDbStats = asyncHandler( + async (req, res, next) => { const { methodName, args = [] } = req.body; if (!methodName || !this.db[methodName]) { return res.error({ @@ -48,13 +48,13 @@ class DiagnosticController { msg: "OK", data: stats, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getDbStats")); - } - } + }, + SERVICE_NAME, + "getDbStats" + ); - async getCPUUsage() { - try { + getCPUUsage = asyncHandler( + async (req, res, next) => { const startUsage = process.cpuUsage(); const timingPeriod = 1000; // measured in ms await new Promise((resolve) => setTimeout(resolve, timingPeriod)); @@ -65,16 +65,13 @@ class DiagnosticController { usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100, }; return cpuUsage; - } catch (error) { - return { - userUsageMs: 0, - systemUsageMs: 0, - }; - } - } + }, + SERVICE_NAME, + "getCPUUsage" + ); - getSystemStats = async (req, res, next) => { - try { + getSystemStats = asyncHandler( + async (req, res, next) => { // Memory Usage const totalMemory = os.totalmem(); const freeMemory = os.freemem(); @@ -129,9 +126,9 @@ class DiagnosticController { msg: "OK", data: diagnostics, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMemoryUsage")); - } - }; + }, + SERVICE_NAME, + "getSystemStats" + ); } export default DiagnosticController; diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js index 3799c58b9..abb6b8123 100755 --- a/server/controllers/inviteController.js +++ b/server/controllers/inviteController.js @@ -3,10 +3,9 @@ import { inviteBodyValidation, inviteVerificationBodyValidation, } from "../validation/joi.js"; -import logger from "../utils/logger.js"; import jwt from "jsonwebtoken"; -import { handleError, handleValidationError } from "./controllerUtils.js"; import { getTokenFromHeaders } from "../utils/utils.js"; +import { asyncHandler, createServerError } from "../utils/errorUtils.js"; const SERVICE_NAME = "inviteController"; @@ -31,99 +30,73 @@ class InviteController { * @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - getInviteToken = async (req, res, next) => { - try { + getInviteToken = asyncHandler( + async (req, res, next) => { // Only admins can invite const token = getTokenFromHeaders(req.headers); const { role, teamId } = jwt.decode(token); req.body.teamId = teamId; - try { - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + await inviteRoleValidation.validateAsync({ roles: role }); + await inviteBodyValidation.validateAsync(req.body); const inviteToken = await this.db.requestInviteToken({ ...req.body }); return res.success({ msg: this.stringService.inviteIssued, data: inviteToken, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } - }; + }, + SERVICE_NAME, + "getInviteToken" + ); - sendInviteEmail = async (req, res, next) => { - try { + sendInviteEmail = asyncHandler( + async (req, res, next) => { // Only admins can invite const token = getTokenFromHeaders(req.headers); const { role, firstname, teamId } = jwt.decode(token); req.body.teamId = teamId; - try { - await inviteRoleValidation.validateAsync({ roles: role }); - await inviteBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + await inviteRoleValidation.validateAsync({ roles: role }); + await inviteBodyValidation.validateAsync(req.body); const inviteToken = await this.db.requestInviteToken({ ...req.body }); const { clientHost } = this.settingsService.getSettings(); - try { - const html = await this.emailService.buildEmail("employeeActivationTemplate", { - name: firstname, - link: `${clientHost}/register/${inviteToken.token}`, - }); - const result = await this.emailService.sendEmail( - req.body.email, - "Welcome to Uptime Monitor", - html + const html = await this.emailService.buildEmail("employeeActivationTemplate", { + name: firstname, + link: `${clientHost}/register/${inviteToken.token}`, + }); + const result = await this.emailService.sendEmail( + req.body.email, + "Welcome to Uptime Monitor", + html + ); + if (!result) { + throw createServerError( + "Failed to send invite e-mail... Please verify your settings." ); - if (!result) { - return res.error({ - msg: "Failed to send invite e-mail... Please verify your settings.", - }); - } - } catch (error) { - logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "sendInviteEmail", - stack: error.stack, - }); } return res.success({ msg: this.stringService.inviteIssued, data: inviteToken, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteController")); - } - }; + }, + SERVICE_NAME, + "sendInviteEmail" + ); - inviteVerifyController = async (req, res, next) => { - try { + inviteVerifyController = asyncHandler( + async (req, res, next) => { await inviteVerificationBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { const invite = await this.db.getInviteToken(req.body.token); - return res.success({ msg: this.stringService.inviteVerified, data: invite, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "inviteVerifyController")); - } - }; + }, + SERVICE_NAME, + "inviteVerifyController" + ); } export default InviteController; diff --git a/server/controllers/logController.js b/server/controllers/logController.js index 5381fd3fa..f4887ce9a 100644 --- a/server/controllers/logController.js +++ b/server/controllers/logController.js @@ -1,23 +1,22 @@ -import { handleError } from "./controllerUtils.js"; +import { asyncHandler } from "../utils/errorUtils.js"; -const SERVICE_NAME = "JobQueueController"; +const SERVICE_NAME = "LogController"; class LogController { constructor(logger) { this.logger = logger; } - getLogs = async (req, res, next) => { - try { + getLogs = asyncHandler( + async (req, res, next) => { const logs = await this.logger.getLogs(); res.success({ msg: "Logs fetched successfully", data: logs, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getLogs")); - return; - } - }; + }, + SERVICE_NAME, + "getLogs" + ); } export default LogController; diff --git a/server/controllers/maintenanceWindowController.js b/server/controllers/maintenanceWindowController.js index 20077cc65..8a19fc79d 100755 --- a/server/controllers/maintenanceWindowController.js +++ b/server/controllers/maintenanceWindowController.js @@ -7,7 +7,7 @@ import { getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, } from "../validation/joi.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "maintenanceWindowController"; @@ -18,14 +18,10 @@ class MaintenanceWindowController { this.stringService = stringService; } - createMaintenanceWindows = async (req, res, next) => { - try { + createMaintenanceWindows = asyncHandler( + async (req, res, next) => { await createMaintenanceWindowBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { + const { teamId } = req.user; const monitorIds = req.body.monitors; const dbTransactions = monitorIds.map((monitorId) => { @@ -44,39 +40,28 @@ class MaintenanceWindowController { return res.success({ msg: this.stringService.maintenanceWindowCreate, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMaintenanceWindow")); - } - }; + }, + SERVICE_NAME, + "createMaintenanceWindows" + ); - getMaintenanceWindowById = async (req, res, next) => { - try { + getMaintenanceWindowById = asyncHandler( + async (req, res, next) => { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id); - return res.success({ msg: this.stringService.maintenanceWindowGetById, data: maintenanceWindow, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById")); - } - }; + }, + SERVICE_NAME, + "getMaintenanceWindowById" + ); - getMaintenanceWindowsByTeamId = async (req, res, next) => { - try { + getMaintenanceWindowsByTeamId = asyncHandler( + async (req, res, next) => { await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { teamId } = req.user; const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId( teamId, @@ -87,20 +72,15 @@ class MaintenanceWindowController { msg: this.stringService.maintenanceWindowGetByTeam, data: maintenanceWindows, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId")); - } - }; + }, + SERVICE_NAME, + "getMaintenanceWindowsByTeamId" + ); - getMaintenanceWindowsByMonitorId = async (req, res, next) => { - try { + getMaintenanceWindowsByMonitorId = asyncHandler( + async (req, res, next) => { await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId( req.params.monitorId ); @@ -109,37 +89,27 @@ class MaintenanceWindowController { msg: this.stringService.maintenanceWindowGetByUser, data: maintenanceWindows, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId")); - } - }; + }, + SERVICE_NAME, + "getMaintenanceWindowsByMonitorId" + ); - deleteMaintenanceWindow = async (req, res, next) => { - try { + deleteMaintenanceWindow = asyncHandler( + async (req, res, next) => { await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { await this.db.deleteMaintenanceWindowById(req.params.id); return res.success({ msg: this.stringService.maintenanceWindowDelete, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow")); - } - }; + }, + SERVICE_NAME, + "deleteMaintenanceWindow" + ); - editMaintenanceWindow = async (req, res, next) => { - try { + editMaintenanceWindow = asyncHandler( + async (req, res, next) => { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const editedMaintenanceWindow = await this.db.editMaintenanceWindowById( req.params.id, req.body @@ -148,10 +118,10 @@ class MaintenanceWindowController { msg: this.stringService.maintenanceWindowEdit, data: editedMaintenanceWindow, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMaintenanceWindow")); - } - }; + }, + SERVICE_NAME, + "editMaintenanceWindow" + ); } export default MaintenanceWindowController; diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js index d32ef463e..f80fa29e5 100755 --- a/server/controllers/monitorController.js +++ b/server/controllers/monitorController.js @@ -16,12 +16,12 @@ import { } from "../validation/joi.js"; import sslChecker from "ssl-checker"; import logger from "../utils/logger.js"; -import { handleError, handleValidationError } from "./controllerUtils.js"; import axios from "axios"; import seedDb from "../db/mongo/utils/seedDb.js"; -const SERVICE_NAME = "monitorController"; import pkg from "papaparse"; +import { asyncHandler, createServerError } from "../utils/errorUtils.js"; +const SERVICE_NAME = "monitorController"; class MonitorController { constructor(db, settingsService, jobQueue, stringService, emailService) { this.db = db; @@ -40,17 +40,17 @@ class MonitorController { * @returns {Promise} * @throws {Error} */ - getAllMonitors = async (req, res, next) => { - try { + getAllMonitors = asyncHandler( + async (req, res, next) => { const monitors = await this.db.getAllMonitors(); return res.success({ msg: this.stringService.monitorGetAll, data: monitors, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMonitors")); - } - }; + }, + SERVICE_NAME, + "getAllMonitors" + ); /** * Returns all monitors with uptime stats for 1,7,30, and 90 days @@ -61,20 +61,20 @@ class MonitorController { * @returns {Promise} * @throws {Error} */ - getAllMonitorsWithUptimeStats = async (req, res, next) => { - try { + getAllMonitorsWithUptimeStats = asyncHandler( + async (req, res, next) => { const monitors = await this.db.getAllMonitorsWithUptimeStats(); return res.success({ msg: this.stringService.monitorGetAll, data: monitors, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats")); - } - }; + }, + SERVICE_NAME, + "getAllMonitorsWithUptimeStats" + ); - getUptimeDetailsById = async (req, res, next) => { - try { + getUptimeDetailsById = asyncHandler( + async (req, res, next) => { const { monitorId } = req.params; const { dateRange, normalize } = req.query; @@ -87,10 +87,10 @@ class MonitorController { msg: this.stringService.monitorGetByIdSuccess, data, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorDetailsById")); - } - }; + }, + SERVICE_NAME, + "getUptimeDetailsById" + ); /** * Returns monitor stats for monitor with matching ID @@ -101,16 +101,11 @@ class MonitorController { * @returns {Promise} * @throws {Error} */ - getMonitorStatsById = async (req, res, next) => { - try { + getMonitorStatsById = asyncHandler( + async (req, res, next) => { await getMonitorStatsByIdParamValidation.validateAsync(req.params); await getMonitorStatsByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; const { monitorId } = req.params; @@ -126,10 +121,10 @@ class MonitorController { msg: this.stringService.monitorStatsById, data: monitorStats, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorStatsById")); - } - }; + }, + SERVICE_NAME, + "getMonitorStatsById" + ); /** * Get hardware details for a specific monitor by ID @@ -140,15 +135,11 @@ class MonitorController { * @returns {Promise} * @throws {Error} - Throws error if monitor not found or other database errors */ - getHardwareDetailsById = async (req, res, next) => { - try { + getHardwareDetailsById = asyncHandler( + async (req, res, next) => { await getHardwareDetailsByIdParamValidation.validateAsync(req.params); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { + const { monitorId } = req.params; const { dateRange } = req.query; const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange }); @@ -156,19 +147,15 @@ class MonitorController { msg: this.stringService.monitorGetByIdSuccess, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getHardwareDetailsById")); - } - }; + }, + SERVICE_NAME, + "getHardwareDetailsById" + ); - getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => { - try { + getMonitorCertificate = asyncHandler( + async (req, res, next, fetchMonitorCertificate) => { await getCertificateParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } - try { const { monitorId } = req.params; const monitor = await this.db.getMonitorById(monitorId); const certificate = await fetchMonitorCertificate(sslChecker, monitor); @@ -179,10 +166,10 @@ class MonitorController { certificateDate: new Date(certificate.validTo), }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorCertificate")); - } - }; + }, + SERVICE_NAME, + "getMonitorCertificate" + ); /** * Retrieves a monitor by its ID. @@ -195,25 +182,20 @@ class MonitorController { * @returns {Object} The response object with a success status, a message, and the retrieved monitor data. * @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422). */ - getMonitorById = async (req, res, next) => { - try { + getMonitorById = asyncHandler( + async (req, res, next) => { await getMonitorByIdParamValidation.validateAsync(req.params); await getMonitorByIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const monitor = await this.db.getMonitorById(req.params.monitorId); return res.success({ msg: this.stringService.monitorGetByIdSuccess, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorById")); - } - }; + }, + SERVICE_NAME, + "getMonitorById" + ); /** * Creates a new monitor and adds it to the job queue. @@ -226,15 +208,10 @@ class MonitorController { * @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - createMonitor = async (req, res, next) => { - try { + createMonitor = asyncHandler( + async (req, res, next) => { await createMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { _id, teamId } = req.user; const monitor = await this.db.createMonitor({ body: req.body, @@ -248,10 +225,10 @@ class MonitorController { msg: this.stringService.monitorCreate, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createMonitor")); - } - }; + }, + SERVICE_NAME, + "createMonitor" + ); /** * Creates bulk monitors and adds them to the job queue after parsing CSV. @@ -263,8 +240,8 @@ class MonitorController { * @returns {Object} The response object with a success status and message. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - createBulkMonitors = async (req, res, next) => { - try { + createBulkMonitors = asyncHandler( + async (req, res, next) => { const { parse } = pkg; // validate the file @@ -310,51 +287,43 @@ class MonitorController { return value; }, complete: async ({ data, errors }) => { - try { - if (errors.length > 0) { - throw new Error("Error parsing CSV"); - } - - if (!data || data.length === 0) { - throw new Error("CSV file contains no data rows"); - } - - const enrichedData = data.map((monitor) => ({ - userId: _id, - teamId, - ...monitor, - description: monitor.description || monitor.name || monitor.url, - name: monitor.name || monitor.url, - type: monitor.type || "http", - })); - - await createMonitorsBodyValidation.validateAsync(enrichedData); - - try { - const monitors = await this.db.createBulkMonitors(enrichedData); - - await Promise.all( - monitors.map(async (monitor, index) => { - this.jobQueue.addJob(monitor._id, monitor); - }) - ); - - return res.success({ - msg: this.stringService.bulkMonitorsCreate, - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createBulkMonitors")); - } - } catch (error) { - next(handleError(error, SERVICE_NAME, "createBulkMonitors")); + if (errors.length > 0) { + throw createServerError("Error parsing CSV"); } + + if (!data || data.length === 0) { + throw createServerError("CSV file contains no data rows"); + } + + const enrichedData = data.map((monitor) => ({ + userId: _id, + teamId, + ...monitor, + description: monitor.description || monitor.name || monitor.url, + name: monitor.name || monitor.url, + type: monitor.type || "http", + })); + + await createMonitorsBodyValidation.validateAsync(enrichedData); + + const monitors = await this.db.createBulkMonitors(enrichedData); + + await Promise.all( + monitors.map(async (monitor, index) => { + this.jobQueue.addJob(monitor._id, monitor); + }) + ); + + return res.success({ + msg: this.stringService.bulkMonitorsCreate, + data: monitors, + }); }, }); - } catch (error) { - return next(handleError(error, SERVICE_NAME, "createBulkMonitors")); - } - }; + }, + SERVICE_NAME, + "createBulkMonitors" + ); /** * Checks if the endpoint can be resolved * @async @@ -365,15 +334,9 @@ class MonitorController { * @returns {Object} The response object with a success status, a message, and the resolution result. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - checkEndpointResolution = async (req, res, next) => { - try { + checkEndpointResolution = asyncHandler( + async (req, res, next) => { await getMonitorURLByQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { const { monitorURL } = req.query; const parsedUrl = new URL(monitorURL); const response = await axios.get(parsedUrl, { @@ -384,10 +347,10 @@ class MonitorController { status: response.status, msg: response.statusText, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); - } - }; + }, + SERVICE_NAME, + "checkEndpointResolution" + ); /** * Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications. @@ -400,24 +363,18 @@ class MonitorController { * @returns {Object} The response object with a success status and a message indicating the deletion of the monitor. * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records. */ - deleteMonitor = async (req, res, next) => { - try { + deleteMonitor = asyncHandler( + async (req, res, next) => { await getMonitorByIdParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { const monitorId = req.params.monitorId; const monitor = await this.db.deleteMonitor({ monitorId }); await this.jobQueue.deleteJob(monitor); await this.db.deleteStatusPagesByMonitorId(monitor._id); return res.success({ msg: this.stringService.monitorDelete }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteMonitor")); - } - }; + }, + SERVICE_NAME, + "deleteMonitor" + ); /** * Deletes all monitors associated with a team. @@ -430,8 +387,8 @@ class MonitorController { * @returns {Object} The response object with a success status and a message indicating the number of deleted monitors. * @throws {Error} If there is an error during the deletion process. */ - deleteAllMonitors = async (req, res, next) => { - try { + deleteAllMonitors = asyncHandler( + async (req, res, next) => { const { teamId } = req.user; const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId); await Promise.all( @@ -442,7 +399,7 @@ class MonitorController { await this.db.deletePageSpeedChecksByMonitorId(monitor._id); await this.db.deleteNotificationsByMonitorId(monitor._id); } catch (error) { - logger.error({ + logger.warn({ message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, service: SERVICE_NAME, method: "deleteAllMonitors", @@ -452,10 +409,10 @@ class MonitorController { }) ); return res.success({ msg: `Deleted ${deletedCount} monitors` }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteAllMonitors")); - } - }; + }, + SERVICE_NAME, + "deleteAllMonitors" + ); /** * Edits a monitor by its ID, updates its notifications, and updates its job in the job queue. @@ -470,16 +427,10 @@ class MonitorController { * @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - editMonitor = async (req, res, next) => { - try { + editMonitor = asyncHandler( + async (req, res, next) => { await getMonitorByIdParamValidation.validateAsync(req.params); await editMonitorBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { const { monitorId } = req.params; const editedMonitor = await this.db.editMonitor(monitorId, req.body); @@ -490,10 +441,10 @@ class MonitorController { msg: this.stringService.monitorEdit, data: editedMonitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editMonitor")); - } - }; + }, + SERVICE_NAME, + "editMonitor" + ); /** * Pauses or resumes a monitor based on its current state. @@ -506,14 +457,10 @@ class MonitorController { * @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data. * @throws {Error} If there is an error during the process. */ - pauseMonitor = async (req, res, next) => { - try { + pauseMonitor = asyncHandler( + async (req, res, next) => { await pauseMonitorParamValidation.validateAsync(req.params); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } - try { const monitorId = req.params.monitorId; const monitor = await this.db.pauseMonitor({ monitorId }); monitor.isActive === true @@ -526,10 +473,10 @@ class MonitorController { : this.stringService.monitorPause, data: monitor, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "pauseMonitor")); - } - }; + }, + SERVICE_NAME, + "pauseMonitor" + ); /** * Adds demo monitors for a team. @@ -542,8 +489,8 @@ class MonitorController { * @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added. * @throws {Error} If there is an error during the process. */ - addDemoMonitors = async (req, res, next) => { - try { + addDemoMonitors = asyncHandler( + async (req, res, next) => { const { _id, teamId } = req.user; const demoMonitors = await this.db.addDemoMonitors(_id, teamId); await Promise.all( @@ -554,10 +501,10 @@ class MonitorController { msg: this.stringService.monitorDemoAdded, data: demoMonitors.length, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "addDemoMonitors")); - } - }; + }, + SERVICE_NAME, + "addDemoMonitors" + ); /** * Sends a test email to verify email delivery functionality. @@ -570,8 +517,8 @@ class MonitorController { * @returns {Object} The response object with a success status and the email delivery message ID. * @throws {Error} If there is an error while sending the test email. */ - sendTestEmail = async (req, res, next) => { - try { + sendTestEmail = asyncHandler( + async (req, res, next) => { const { to } = req.body; if (!to || typeof to !== "string") { throw new Error(this.stringService.errorForValidEmailAddress); @@ -584,29 +531,23 @@ class MonitorController { const messageId = await this.emailService.sendEmail(to, subject, html); if (!messageId) { - return res.error({ - msg: "Failed to send test email.", - }); + throw createServerError("Failed to send test email."); } return res.success({ msg: this.stringService.sendTestEmail, data: { messageId }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "sendTestEmail")); - } - }; + }, + SERVICE_NAME, + "sendTestEmail" + ); - getMonitorsByTeamId = async (req, res, next) => { - try { + getMonitorsByTeamId = asyncHandler( + async (req, res, next) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - } - try { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; const teamId = req.user.teamId; @@ -624,20 +565,16 @@ class MonitorController { msg: this.stringService.monitorGetByTeamId, data: monitors, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); - } - }; + }, + SERVICE_NAME, + "getMonitorsByTeamId" + ); - getMonitorsAndSummaryByTeamId = async (req, res, next) => { - try { + getMonitorsAndSummaryByTeamId = asyncHandler( + async (req, res, next) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - return next(handleValidationError(error, SERVICE_NAME)); - } - try { const { explain } = req; const { type } = req.query; const { teamId } = req.user; @@ -651,20 +588,16 @@ class MonitorController { msg: "OK", // TODO data: result, }); - } catch (error) { - return next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId")); - } - }; + }, + SERVICE_NAME, + "getMonitorsAndSummaryByTeamId" + ); - getMonitorsWithChecksByTeamId = async (req, res, next) => { - try { + getMonitorsWithChecksByTeamId = asyncHandler( + async (req, res, next) => { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - return next(handleValidationError(error, SERVICE_NAME)); - } - try { const { explain } = req; let { limit, type, page, rowsPerPage, filter, field, order } = req.query; const { teamId } = req.user; @@ -684,23 +617,23 @@ class MonitorController { msg: "OK", data: result, }); - } catch (error) { - return next(handleError(error, SERVICE_NAME, "getMonitorsWithChecksByTeamId")); - } - }; + }, + SERVICE_NAME, + "getMonitorsWithChecksByTeamId" + ); - seedDb = async (req, res, next) => { - try { + seedDb = asyncHandler( + async (req, res, next) => { const { _id, teamId } = req.user; await seedDb(_id, teamId); res.success({ msg: "Database seeded" }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "seedDb")); - } - }; + }, + SERVICE_NAME, + "seedDb" + ); - exportMonitorsToCSV = async (req, res, next) => { - try { + exportMonitorsToCSV = asyncHandler( + async (req, res, next) => { const { teamId } = req.user; const monitors = await this.db.getMonitorsByTeamId({ teamId }); @@ -730,10 +663,10 @@ class MonitorController { "Content-Disposition": "attachment; filename=monitors.csv", }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV")); - } - }; + }, + SERVICE_NAME, + "exportMonitorsToCSV" + ); } export default MonitorController; diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index e7e2a1f2a..4e21e7284 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -1,22 +1,8 @@ -import { - triggerNotificationBodyValidation, - createNotificationBodyValidation, -} from "../validation/joi.js"; -import { handleError, handleValidationError } from "./controllerUtils.js"; +import { createNotificationBodyValidation } from "../validation/joi.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "NotificationController"; -const NOTIFICATION_TYPES = { - WEBHOOK: "webhook", - TELEGRAM: "telegram", -}; - -const PLATFORMS = { - SLACK: "slack", - DISCORD: "discord", - TELEGRAM: "telegram", -}; - class NotificationController { constructor({ notificationService, stringService, statusService, db }) { this.notificationService = notificationService; @@ -25,8 +11,8 @@ class NotificationController { this.db = db; } - testNotification = async (req, res, next) => { - try { + testNotification = asyncHandler( + async (req, res, next) => { const notification = req.body; const success = await this.notificationService.sendTestNotification(notification); @@ -41,22 +27,17 @@ class NotificationController { return res.success({ msg: "Notification sent successfully", }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "testWebhook")); - } - }; + }, + SERVICE_NAME, + "testNotification" + ); - createNotification = async (req, res, next) => { - try { + createNotification = asyncHandler( + async (req, res, next) => { await createNotificationBodyValidation.validateAsync(req.body, { abortEarly: false, }); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const body = req.body; const { _id, teamId } = req.user; body.userId = _id; @@ -66,69 +47,64 @@ class NotificationController { msg: "Notification created successfully", data: notification, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createNotification")); - } - }; + }, + SERVICE_NAME, + "createNotification" + ); - getNotificationsByTeamId = async (req, res, next) => { - try { + getNotificationsByTeamId = asyncHandler( + async (req, res, next) => { const notifications = await this.db.getNotificationsByTeamId(req.user.teamId); return res.success({ msg: "Notifications fetched successfully", data: notifications, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getNotificationsByTeamId")); - } - }; + }, + SERVICE_NAME, + "getNotificationsByTeamId" + ); - deleteNotification = async (req, res, next) => { - try { + deleteNotification = asyncHandler( + async (req, res, next) => { await this.db.deleteNotificationById(req.params.id); return res.success({ msg: "Notification deleted successfully", }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteNotification")); - } - }; + }, + SERVICE_NAME, + "deleteNotification" + ); - getNotificationById = async (req, res, next) => { - try { + getNotificationById = asyncHandler( + async (req, res, next) => { const notification = await this.db.getNotificationById(req.params.id); return res.success({ msg: "Notification fetched successfully", data: notification, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getNotificationById")); - } - }; + }, + SERVICE_NAME, + "getNotificationById" + ); - editNotification = async (req, res, next) => { - try { + editNotification = asyncHandler( + async (req, res, next) => { await createNotificationBodyValidation.validateAsync(req.body, { abortEarly: false, }); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const notification = await this.db.editNotification(req.params.id, req.body); return res.success({ msg: "Notification updated successfully", data: notification, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "editNotification")); - } - }; + }, + SERVICE_NAME, + "editNotification" + ); - testAllNotifications = async (req, res, next) => { - try { + testAllNotifications = asyncHandler( + async (req, res, next) => { const { monitorId } = req.body; const monitor = await this.db.getMonitorById(monitorId); const notifications = monitor.notifications; @@ -138,10 +114,10 @@ class NotificationController { return res.success({ msg: "All notifications sent successfully", }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "testAllNotifications")); - } - }; + }, + SERVICE_NAME, + "testAllNotifications" + ); } export default NotificationController; diff --git a/server/controllers/queueController.js b/server/controllers/queueController.js index 968106a5a..faa0febd7 100755 --- a/server/controllers/queueController.js +++ b/server/controllers/queueController.js @@ -1,4 +1,4 @@ -import { handleError } from "./controllerUtils.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "JobQueueController"; @@ -8,82 +8,76 @@ class JobQueueController { this.stringService = stringService; } - getMetrics = async (req, res, next) => { - try { + getMetrics = asyncHandler( + async (req, res, next) => { const metrics = await this.jobQueue.getMetrics(); res.success({ msg: this.stringService.queueGetMetrics, data: metrics, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMetrics")); - return; - } - }; + }, + SERVICE_NAME, + "getMetrics" + ); - getJobs = async (req, res, next) => { - try { + getJobs = asyncHandler( + async (req, res, next) => { const jobs = await this.jobQueue.getJobs(); return res.success({ msg: this.stringService.queueGetJobs, data: jobs, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getJobs")); - return; - } - }; + }, + SERVICE_NAME, + "getJobs" + ); - getAllMetrics = async (req, res, next) => { - try { + getAllMetrics = asyncHandler( + async (req, res, next) => { const jobs = await this.jobQueue.getJobs(); const metrics = await this.jobQueue.getMetrics(); return res.success({ msg: this.stringService.queueGetAllMetrics, data: { jobs, metrics }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAllMetrics")); - return; - } - }; + }, + SERVICE_NAME, + "getAllMetrics" + ); - addJob = async (req, res, next) => { - try { + addJob = asyncHandler( + async (req, res, next) => { await this.jobQueue.addJob(Math.random().toString(36).substring(7)); return res.success({ msg: this.stringService.queueAddJob, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "addJob")); - return; - } - }; + }, + SERVICE_NAME, + "addJob" + ); - flushQueue = async (req, res, next) => { - try { + flushQueue = asyncHandler( + async (req, res, next) => { const result = await this.jobQueue.flushQueues(); return res.success({ msg: this.stringService.jobQueueFlush, data: result, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "flushQueue")); - return; - } - }; + }, + SERVICE_NAME, + "flushQueue" + ); - checkQueueHealth = async (req, res, next) => { - try { + checkQueueHealth = asyncHandler( + async (req, res, next) => { const stuckQueues = await this.jobQueue.checkQueueHealth(); return res.success({ msg: this.stringService.queueHealthCheck, data: stuckQueues, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "checkQueueHealth")); - return; - } - }; + }, + SERVICE_NAME, + "checkQueueHealth" + ); } export default JobQueueController; diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index ef1d689d6..798fbdd5e 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -1,6 +1,7 @@ import { updateAppSettingsBodyValidation } from "../validation/joi.js"; -import { handleValidationError, handleError } from "./controllerUtils.js"; import { sendTestEmailBodyValidation } from "../validation/joi.js"; +import { asyncHandler, createServerError } from "../utils/errorUtils.js"; + const SERVICE_NAME = "SettingsController"; class SettingsController { @@ -32,45 +33,44 @@ class SettingsController { return returnSettings; }; - getAppSettings = async (req, res, next) => { - const dbSettings = await this.settingsService.getDBSettings(); + getAppSettings = asyncHandler( + async (req, res, next) => { + const dbSettings = await this.settingsService.getDBSettings(); - const returnSettings = this.buildAppSettings(dbSettings); - return res.success({ - msg: this.stringService.getAppSettings, - data: returnSettings, - }); - }; + const returnSettings = this.buildAppSettings(dbSettings); + return res.success({ + msg: this.stringService.getAppSettings, + data: returnSettings, + }); + }, + SERVICE_NAME, + "getAppSettings" + ); - updateAppSettings = async (req, res, next) => { - try { + updateAppSettings = asyncHandler( + async (req, res, next) => { await updateAppSettingsBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const updatedSettings = await this.db.updateAppSettings(req.body); const returnSettings = this.buildAppSettings(updatedSettings); return res.success({ msg: this.stringService.updateAppSettings, data: returnSettings, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateAppSettings")); - } - }; + }, + SERVICE_NAME, + "updateAppSettings" + ); - sendTestEmail = async (req, res, next) => { - try { - await sendTestEmailBodyValidation.validateAsync(req.body); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } + sendTestEmail = asyncHandler( + async (req, res, next) => { + try { + await sendTestEmailBodyValidation.validateAsync(req.body); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } - try { const { to, systemEmailHost, @@ -107,20 +107,17 @@ class SettingsController { }); if (!messageId) { - return res.error({ - msg: "Failed to send test email.", - }); + throw createServerError("Failed to send test email."); } return res.success({ msg: this.stringService.sendTestEmail, data: { messageId }, }); - } catch (error) { - next(handleError(error, SERVICE_NAME)); - return; - } - }; + }, + SERVICE_NAME, + "sendTestEmail" + ); } export default SettingsController; diff --git a/server/controllers/statusPageController.js b/server/controllers/statusPageController.js index 577cf4997..1d3dd96fb 100755 --- a/server/controllers/statusPageController.js +++ b/server/controllers/statusPageController.js @@ -1,10 +1,10 @@ -import { handleError, handleValidationError } from "./controllerUtils.js"; import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation, } from "../validation/joi.js"; +import { asyncHandler } from "../utils/errorUtils.js"; const SERVICE_NAME = "statusPageController"; @@ -14,16 +14,11 @@ class StatusPageController { this.stringService = stringService; } - createStatusPage = async (req, res, next) => { - try { + createStatusPage = asyncHandler( + async (req, res, next) => { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const { _id, teamId } = req.user; const statusPage = await this.db.createStatusPage({ statusPageData: req.body, @@ -35,21 +30,16 @@ class StatusPageController { msg: this.stringService.statusPageCreate, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "createStatusPage")); - } - }; + }, + SERVICE_NAME, + "createStatusPage" + ); - updateStatusPage = async (req, res, next) => { - try { + updateStatusPage = asyncHandler( + async (req, res, next) => { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const statusPage = await this.db.updateStatusPage(req.body, req.file); if (statusPage === null) { const error = new Error(this.stringService.statusPageNotFound); @@ -60,45 +50,40 @@ class StatusPageController { msg: this.stringService.statusPageUpdate, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "updateStatusPage")); - } - }; + }, + SERVICE_NAME, + "updateStatusPage" + ); - getStatusPage = async (req, res, next) => { - try { + getStatusPage = asyncHandler( + async (req, res, next) => { const statusPage = await this.db.getStatusPage(); return res.success({ msg: this.stringService.statusPageByUrl, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPage")); - } - }; + }, + SERVICE_NAME, + "getStatusPage" + ); - getStatusPageByUrl = async (req, res, next) => { - try { + getStatusPageByUrl = asyncHandler( + async (req, res, next) => { await getStatusPageParamValidation.validateAsync(req.params); await getStatusPageQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - try { const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type); return res.success({ msg: this.stringService.statusPageByUrl, data: statusPage, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPageByUrl")); - } - }; + }, + SERVICE_NAME, + "getStatusPageByUrl" + ); - getStatusPagesByTeamId = async (req, res, next) => { - try { + getStatusPagesByTeamId = asyncHandler( + async (req, res, next) => { const teamId = req.user.teamId; const statusPages = await this.db.getStatusPagesByTeamId(teamId); @@ -106,21 +91,21 @@ class StatusPageController { msg: this.stringService.statusPageByTeamId, data: statusPages, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getStatusPageByTeamId")); - } - }; + }, + SERVICE_NAME, + "getStatusPagesByTeamId" + ); - deleteStatusPage = async (req, res, next) => { - try { + deleteStatusPage = asyncHandler( + async (req, res, next) => { await this.db.deleteStatusPage(req.params.url); return res.success({ msg: this.stringService.statusPageDelete, }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "deleteStatusPage")); - } - }; + }, + SERVICE_NAME, + "deleteStatusPage" + ); } export default StatusPageController; diff --git a/server/middleware/handleErrors.js b/server/middleware/handleErrors.js index 87895fcf2..f04df1a08 100755 --- a/server/middleware/handleErrors.js +++ b/server/middleware/handleErrors.js @@ -3,6 +3,7 @@ import ServiceRegistry from "../service/serviceRegistry.js"; import StringService from "../service/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/utils/errorUtils.js b/server/utils/errorUtils.js new file mode 100644 index 000000000..fc328690a --- /dev/null +++ b/server/utils/errorUtils.js @@ -0,0 +1,121 @@ +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); + } + + // 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); + } + }; +};