add error handling util, update middleware and controllers

This commit is contained in:
Alex Holliday
2025-07-15 16:33:35 -07:00
parent c54feb9306
commit 38bb915144
15 changed files with 683 additions and 852 deletions

View File

@@ -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<void>} 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Express.Response>}
* @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<Express.Response>}
* @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<Express.Response>}
* @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<Express.Response>}
* @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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

121
server/utils/errorUtils.js Normal file
View File

@@ -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);
}
};
};