Merge pull request #2681 from bluewave-labs/feat/error-service

feat: error service
This commit is contained in:
Alexander Holliday
2025-07-25 10:36:10 -07:00
committed by GitHub
24 changed files with 464 additions and 417 deletions

View File

@@ -1,5 +1,4 @@
import { createAnnouncementValidation } from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
const SERVICE_NAME = "announcementController";
@@ -10,10 +9,9 @@ const SERVICE_NAME = "announcementController";
* @class AnnouncementController
*/
class AnnouncementController {
constructor(db, stringService) {
this.db = db;
this.stringService = stringService;
class AnnouncementController extends BaseController {
constructor(commonDependencies) {
super(commonDependencies);
this.createAnnouncement = this.createAnnouncement.bind(this);
this.getAnnouncement = this.getAnnouncement.bind(this);
}

View File

@@ -1,3 +1,4 @@
import BaseController from "./baseController.js";
import {
registrationBodyValidation,
loginValidation,
@@ -10,7 +11,6 @@ import {
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
} from "../validation/joi.js";
import { asyncHandler, createError } from "../utils/errorUtils.js";
const SERVICE_NAME = "authController";
@@ -23,26 +23,22 @@ const SERVICE_NAME = "authController";
* @class AuthController
* @description Manages user authentication and authorization operations
*/
class AuthController {
class AuthController extends BaseController {
/**
* Creates an instance of AuthController.
*
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.db - Database service for data operations
* @param {Object} dependencies.settingsService - Service for application settings
* @param {Object} dependencies.emailService - Service for email operations
* @param {Object} dependencies.jobQueue - Service for job queue operations
* @param {Object} dependencies.stringService - Service for string/localization
* @param {Object} dependencies.logger - Logger service
* @param {Object} dependencies.userService - User business logic service
*/
constructor({ db, settingsService, emailService, jobQueue, stringService, logger, userService }) {
this.db = db;
constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.emailService = emailService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.logger = logger;
this.userService = userService;
}
@@ -85,7 +81,7 @@ class AuthController {
* "inviteToken": "abc123..."
* }
*/
registerUser = asyncHandler(
registerUser = this.asyncHandler(
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
@@ -121,7 +117,7 @@ class AuthController {
* "password": "SecurePass123!"
* }
*/
loginUser = asyncHandler(
loginUser = this.asyncHandler(
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
@@ -173,7 +169,7 @@ class AuthController {
* "newPassword": "NewPass123!"
* }
*/
editUser = asyncHandler(
editUser = this.asyncHandler(
async (req, res) => {
await editUserBodyValidation.validateAsync(req.body);
@@ -200,7 +196,7 @@ class AuthController {
* GET /auth/users/superadmin
* // Response: { "data": true } or { "data": false }
*/
checkSuperadminExists = asyncHandler(
checkSuperadminExists = this.asyncHandler(
async (req, res) => {
const superAdminExists = await this.userService.checkSuperadminExists();
return res.success({
@@ -230,7 +226,7 @@ class AuthController {
* "email": "john@example.com"
* }
*/
requestRecovery = asyncHandler(
requestRecovery = this.asyncHandler(
async (req, res) => {
await recoveryValidation.validateAsync(req.body);
const email = req?.body?.email;
@@ -262,7 +258,7 @@ class AuthController {
* "recoveryToken": "abc123..."
* }
*/
validateRecovery = asyncHandler(
validateRecovery = this.asyncHandler(
async (req, res) => {
await recoveryTokenBodyValidation.validateAsync(req.body);
await this.userService.validateRecovery(req.body.recoveryToken);
@@ -294,7 +290,7 @@ class AuthController {
* "recoveryToken": "abc123..."
* }
*/
resetPassword = asyncHandler(
resetPassword = this.asyncHandler(
async (req, res) => {
await newPasswordValidation.validateAsync(req.body);
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
@@ -326,7 +322,7 @@ class AuthController {
* DELETE /auth/user
* // Requires JWT authentication
*/
deleteUser = asyncHandler(
deleteUser = this.asyncHandler(
async (req, res) => {
await this.userService.deleteUser(req.user);
return res.success({
@@ -350,7 +346,7 @@ class AuthController {
* GET /auth/users
* // Requires JWT authentication with admin/superadmin role
*/
getAllUsers = asyncHandler(
getAllUsers = this.asyncHandler(
async (req, res) => {
const allUsers = await this.userService.getAllUsers();
return res.success({
@@ -381,7 +377,7 @@ class AuthController {
* GET /auth/users/507f1f77bcf86cd799439011
* // Requires JWT authentication with superadmin role
*/
getUserById = asyncHandler(
getUserById = this.asyncHandler(
async (req, res) => {
await getUserByIdParamValidation.validateAsync(req.params);
const userId = req?.params?.userId;
@@ -431,7 +427,7 @@ class AuthController {
* }
* // Requires JWT authentication with superadmin role
*/
editUserById = asyncHandler(
editUserById = this.asyncHandler(
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {

View File

@@ -0,0 +1,83 @@
import { AppError } from "../service/infrastructure/errorService.js";
export const createCommonDependencies = (serviceRegistry, dbServiceName, loggerServiceName, errorServiceName, stringServiceName) => {
return {
db: serviceRegistry.get(dbServiceName),
errorService: serviceRegistry.get(errorServiceName),
logger: serviceRegistry.get(loggerServiceName),
stringService: serviceRegistry.get(stringServiceName),
};
};
class BaseController {
constructor({ db, logger, errorService, ...additionalDependencies }) {
this.db = db;
this.logger = logger;
this.errorService = errorService;
Object.assign(this, additionalDependencies);
this.asyncHandler = (fn, serviceName, methodName) => {
return async (req, res, next) => {
try {
await fn(req, res, next);
} catch (error) {
// Handle validation errors
if (error.isJoi) {
const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName);
return next(validationError);
}
if (error.name === "ValidationError") {
const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName);
return next(validationError);
}
if (error.name === "CastError") {
const notFoundError = this.errorService.createNotFoundError(
"Invalid resource identifier",
{ field: error.path, value: error.value },
serviceName,
methodName
);
return next(notFoundError);
}
if (error.code === "11000") {
const conflictError = this.errorService.createConflictError("Resource already exists", {
originalError: error.message,
code: error.code,
});
conflictError.service = serviceName;
conflictError.method = methodName;
return next(conflictError);
}
if (error instanceof AppError) {
error.service = error.service || serviceName;
error.method = error.method || methodName;
return next(error);
}
if (error.status) {
const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, {
originalError: error.message,
stack: error.stack,
});
return next(appError);
}
// For unknown errors, create a server error
const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", {
originalError: error.message,
stack: error.stack,
});
appError.service = serviceName;
appError.method = methodName;
appError.stack = error.stack; // Preserve original stack
return next(appError);
}
};
};
}
}
export default BaseController;

View File

@@ -1,3 +1,4 @@
import BaseController from "./baseController.js";
import {
getChecksParamValidation,
getChecksQueryValidation,
@@ -9,7 +10,6 @@ import {
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
} from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
const SERVICE_NAME = "checkController";
@@ -22,20 +22,18 @@ const SERVICE_NAME = "checkController";
* @class CheckController
* @description Manages check operations and monitoring data
*/
class CheckController {
class CheckController extends BaseController {
/**
* Creates an instance of CheckController.
*
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.db - Database service for data operations
* @param {Object} dependencies.settingsService - Service for application settings
* @param {Object} dependencies.stringService - Service for string/localization
* @param {Object} dependencies.checkService - Check business logic service
*/
constructor({ db, settingsService, stringService, checkService }) {
this.db = db;
constructor(commonDependencies, { settingsService, checkService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.checkService = checkService;
}
@@ -67,7 +65,7 @@ class CheckController {
* GET /checks/monitor/507f1f77bcf86cd799439011?page=1&rowsPerPage=10&status=down
* // Requires JWT authentication
*/
getChecksByMonitor = asyncHandler(
getChecksByMonitor = this.asyncHandler(
async (req, res) => {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
@@ -109,7 +107,7 @@ class CheckController {
* GET /checks/team?page=1&rowsPerPage=20&status=down&ack=false
* // Requires JWT authentication
*/
getChecksByTeam = asyncHandler(
getChecksByTeam = this.asyncHandler(
async (req, res) => {
await getTeamChecksQueryValidation.validateAsync(req.query);
const checkData = await this.checkService.getChecksByTeam({
@@ -140,7 +138,7 @@ class CheckController {
* // Requires JWT authentication
* // Response includes counts by status, time ranges, etc.
*/
getChecksSummaryByTeamId = asyncHandler(
getChecksSummaryByTeamId = this.asyncHandler(
async (req, res) => {
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId });
return res.success({
@@ -176,7 +174,7 @@ class CheckController {
* }
* // Requires JWT authentication
*/
ackCheck = asyncHandler(
ackCheck = this.asyncHandler(
async (req, res) => {
await ackCheckBodyValidation.validateAsync(req.body);
@@ -220,7 +218,7 @@ class CheckController {
* }
* // Requires JWT authentication
*/
ackAllChecks = asyncHandler(
ackAllChecks = this.asyncHandler(
async (req, res) => {
await ackAllChecksParamValidation.validateAsync(req.params);
await ackAllChecksBodyValidation.validateAsync(req.body);
@@ -266,7 +264,7 @@ class CheckController {
* // Requires JWT authentication
* // Response: { "data": { "deletedCount": 150 } }
*/
deleteChecks = asyncHandler(
deleteChecks = this.asyncHandler(
async (req, res) => {
await deleteChecksParamValidation.validateAsync(req.params);
@@ -300,7 +298,7 @@ class CheckController {
* // Requires JWT authentication
* // Response: { "data": { "deletedCount": 1250 } }
*/
deleteChecksByTeamId = asyncHandler(
deleteChecksByTeamId = this.asyncHandler(
async (req, res) => {
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
@@ -336,7 +334,7 @@ class CheckController {
* // Requires JWT authentication
* // Sets check TTL to 30 days
*/
updateChecksTTL = asyncHandler(
updateChecksTTL = this.asyncHandler(
async (req, res) => {
await updateChecksTTLBodyValidation.validateAsync(req.body);

View File

@@ -1,12 +1,10 @@
import { createServerError } from "../utils/errorUtils.js";
const fetchMonitorCertificate = async (sslChecker, monitor) => {
const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname;
const cert = await sslChecker(hostname);
// Throw an error if no cert or if cert.validTo is not present
if (cert?.validTo === null || cert?.validTo === undefined) {
throw createServerError("Certificate not found");
throw new Error("Certificate not found");
}
return cert;
};

View File

@@ -1,6 +1,5 @@
import { asyncHandler } from "../utils/errorUtils.js";
const SERVICE_NAME = "diagnosticController";
import BaseController from "./baseController.js";
/**
* Diagnostic Controller
*
@@ -10,14 +9,15 @@ const SERVICE_NAME = "diagnosticController";
* @class DiagnosticController
* @description Manages system diagnostics and performance monitoring
*/
class DiagnosticController {
class DiagnosticController extends BaseController {
/**
* Creates an instance of DiagnosticController.
*
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.diagnosticService - Service for system diagnostics and monitoring
*/
constructor({ diagnosticService }) {
constructor(commonDependencies, { diagnosticService }) {
super(commonDependencies);
this.diagnosticService = diagnosticService;
}
@@ -41,7 +41,7 @@ class DiagnosticController {
* // - Database connection status
* // - Active processes/connections
*/
getSystemStats = asyncHandler(
getSystemStats = this.asyncHandler(
async (req, res) => {
const diagnostics = await this.diagnosticService.getSystemStats();
return res.success({

View File

@@ -1,21 +1,19 @@
import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "inviteController";
/**
* Controller for handling user invitation operations
* Manages invite token generation, email sending, and token verification
*/
class InviteController {
class InviteController extends BaseController {
/**
* Creates a new InviteController instance
* @param {Object} dependencies - Dependencies injected into the controller
* @param {Object} dependencies.stringService - Service for internationalized strings
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies.inviteService - Service for invite-related operations
*/
constructor({ stringService, inviteService }) {
this.stringService = stringService;
constructor(commonDependencies, { inviteService }) {
super(commonDependencies);
this.inviteService = inviteService;
}
@@ -28,7 +26,7 @@ class InviteController {
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with invite token data
*/
getInviteToken = asyncHandler(
getInviteToken = this.asyncHandler(
async (req, res) => {
const invite = req.body;
const teamId = req?.user?.teamId;
@@ -54,7 +52,7 @@ class InviteController {
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with invite token data
*/
sendInviteEmail = asyncHandler(
sendInviteEmail = this.asyncHandler(
async (req, res) => {
const inviteRequest = req.body;
inviteRequest.teamId = req?.user?.teamId;
@@ -81,7 +79,7 @@ class InviteController {
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with verified invite data
*/
verifyInviteToken = asyncHandler(
verifyInviteToken = this.asyncHandler(
async (req, res) => {
await inviteVerificationBodyValidation.validateAsync(req.body);
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });

View File

@@ -1,14 +1,13 @@
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "LogController";
class LogController {
constructor(logger) {
this.logger = logger;
class LogController extends BaseController {
constructor(commonDependencies) {
super(commonDependencies);
}
getLogs = asyncHandler(
async (req, res, next) => {
getLogs = this.asyncHandler(
async (req, res) => {
const logs = await this.logger.getLogs();
res.success({
msg: "Logs fetched successfully",

View File

@@ -7,25 +7,24 @@ import {
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "maintenanceWindowController";
class MaintenanceWindowController {
constructor({ db, settingsService, stringService, maintenanceWindowService }) {
this.db = db;
class MaintenanceWindowController extends BaseController {
constructor(commonDependencies, { settingsService, maintenanceWindowService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.maintenanceWindowService = maintenanceWindowService;
}
createMaintenanceWindows = asyncHandler(
createMaintenanceWindows = this.asyncHandler(
async (req, res) => {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
@@ -38,13 +37,13 @@ class MaintenanceWindowController {
"createMaintenanceWindows"
);
getMaintenanceWindowById = asyncHandler(
getMaintenanceWindowById = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req.user.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId });
@@ -58,14 +57,14 @@ class MaintenanceWindowController {
"getMaintenanceWindowById"
);
getMaintenanceWindowsByTeamId = asyncHandler(
getMaintenanceWindowsByTeamId = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
@@ -79,13 +78,13 @@ class MaintenanceWindowController {
"getMaintenanceWindowsByTeamId"
);
getMaintenanceWindowsByMonitorId = asyncHandler(
getMaintenanceWindowsByMonitorId = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId });
@@ -99,13 +98,13 @@ class MaintenanceWindowController {
"getMaintenanceWindowsByMonitorId"
);
deleteMaintenanceWindow = asyncHandler(
deleteMaintenanceWindow = this.asyncHandler(
async (req, res) => {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId });
@@ -118,14 +117,14 @@ class MaintenanceWindowController {
"deleteMaintenanceWindow"
);
editMaintenanceWindow = asyncHandler(
editMaintenanceWindow = this.asyncHandler(
async (req, res) => {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
const teamId = req.user.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId });

View File

@@ -13,16 +13,15 @@ import {
getHardwareDetailsByIdQueryValidation,
} from "../validation/joi.js";
import sslChecker from "ssl-checker";
import { asyncHandler } from "../utils/errorUtils.js";
import { fetchMonitorCertificate } from "./controllerUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "monitorController";
class MonitorController {
constructor({ db, settingsService, jobQueue, stringService, emailService, monitorService }) {
this.db = db;
class MonitorController extends BaseController {
constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
this.monitorService = monitorService;
}
@@ -30,13 +29,11 @@ class MonitorController {
async verifyTeamAccess(teamId, monitorId) {
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
}
getAllMonitors = asyncHandler(
getAllMonitors = this.asyncHandler(
async (req, res) => {
const monitors = await this.monitorService.getAllMonitors();
return res.success({
@@ -48,7 +45,7 @@ class MonitorController {
"getAllMonitors"
);
getUptimeDetailsById = asyncHandler(
getUptimeDetailsById = this.asyncHandler(
async (req, res) => {
const monitorId = req?.params?.monitorId;
const dateRange = req?.query?.dateRange;
@@ -57,7 +54,7 @@ class MonitorController {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const data = await this.monitorService.getUptimeDetailsById({
@@ -75,7 +72,7 @@ class MonitorController {
"getUptimeDetailsById"
);
getMonitorStatsById = asyncHandler(
getMonitorStatsById = this.asyncHandler(
async (req, res) => {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
@@ -85,7 +82,7 @@ class MonitorController {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitorStats = await this.monitorService.getMonitorStatsById({
@@ -116,7 +113,7 @@ class MonitorController {
* @returns {Promise<Express.Response>}
* @throws {Error} - Throws error if monitor not found or other database errors
*/
getHardwareDetailsById = asyncHandler(
getHardwareDetailsById = this.asyncHandler(
async (req, res) => {
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
@@ -125,7 +122,7 @@ class MonitorController {
const dateRange = req?.query?.dateRange;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.getHardwareDetailsById({
@@ -143,8 +140,8 @@ class MonitorController {
"getHardwareDetailsById"
);
getMonitorCertificate = asyncHandler(
async (req, res, next) => {
getMonitorCertificate = this.asyncHandler(
async (req, res) => {
await getCertificateParamValidation.validateAsync(req.params);
const { monitorId } = req.params;
@@ -162,14 +159,14 @@ class MonitorController {
"getMonitorCertificate"
);
getMonitorById = asyncHandler(
getMonitorById = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
@@ -183,7 +180,7 @@ class MonitorController {
"getMonitorById"
);
createMonitor = asyncHandler(
createMonitor = this.asyncHandler(
async (req, res) => {
await createMonitorBodyValidation.validateAsync(req.body);
@@ -201,30 +198,30 @@ class MonitorController {
"createMonitor"
);
createBulkMonitors = asyncHandler(
createBulkMonitors = this.asyncHandler(
async (req, res) => {
if (!req.file) {
throw new Error("No file uploaded");
throw this.errorService.createBadRequestError("No file uploaded");
}
if (!req.file.mimetype.includes("csv")) {
throw new Error("File is not a CSV");
throw this.errorService.createBadRequestError("File is not a CSV");
}
if (req.file.size === 0) {
throw new Error("File is empty");
throw this.errorService.createBadRequestError("File is empty");
}
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
if (!userId || !teamId) {
throw new Error("Missing userId or teamId");
throw this.errorService.createBadRequestError("Missing userId or teamId");
}
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
throw new Error("Cannot get file from buffer");
throw this.errorService.createBadRequestError("Cannot get file from buffer");
}
const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId });
@@ -238,13 +235,13 @@ class MonitorController {
"createBulkMonitors"
);
deleteMonitor = asyncHandler(
deleteMonitor = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
const monitorId = req.params.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -255,11 +252,11 @@ class MonitorController {
"deleteMonitor"
);
deleteAllMonitors = asyncHandler(
deleteAllMonitors = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
@@ -270,7 +267,7 @@ class MonitorController {
"deleteAllMonitors"
);
editMonitor = asyncHandler(
editMonitor = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
@@ -278,7 +275,7 @@ class MonitorController {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
@@ -292,14 +289,14 @@ class MonitorController {
"editMonitor"
);
pauseMonitor = asyncHandler(
pauseMonitor = this.asyncHandler(
async (req, res) => {
await pauseMonitorParamValidation.validateAsync(req.params);
const monitorId = req.params.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -313,7 +310,7 @@ class MonitorController {
"pauseMonitor"
);
addDemoMonitors = asyncHandler(
addDemoMonitors = this.asyncHandler(
async (req, res) => {
const { _id, teamId } = req.user;
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
@@ -327,11 +324,11 @@ class MonitorController {
"addDemoMonitors"
);
sendTestEmail = asyncHandler(
sendTestEmail = this.asyncHandler(
async (req, res) => {
const { to } = req.body;
if (!to || typeof to !== "string") {
throw new Error(this.stringService.errorForValidEmailAddress);
throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress);
}
const messageId = await this.monitorService.sendTestEmail({ to });
@@ -344,8 +341,8 @@ class MonitorController {
"sendTestEmail"
);
getMonitorsByTeamId = asyncHandler(
async (req, res, next) => {
getMonitorsByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
@@ -363,7 +360,7 @@ class MonitorController {
"getMonitorsByTeamId"
);
getMonitorsAndSummaryByTeamId = asyncHandler(
getMonitorsAndSummaryByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
@@ -372,7 +369,7 @@ class MonitorController {
const type = req?.query?.type;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
@@ -386,7 +383,7 @@ class MonitorController {
"getMonitorsAndSummaryByTeamId"
);
getMonitorsWithChecksByTeamId = asyncHandler(
getMonitorsWithChecksByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
@@ -395,7 +392,7 @@ class MonitorController {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
@@ -419,11 +416,11 @@ class MonitorController {
"getMonitorsWithChecksByTeamId"
);
exportMonitorsToCSV = asyncHandler(
exportMonitorsToCSV = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const csv = await this.monitorService.exportMonitorsToCSV({ teamId });

View File

@@ -1,27 +1,23 @@
import { createNotificationBodyValidation } from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "NotificationController";
class NotificationController {
constructor({ notificationService, stringService, statusService, db }) {
class NotificationController extends BaseController {
constructor(commonDependencies, { notificationService, statusService }) {
super(commonDependencies);
this.notificationService = notificationService;
this.stringService = stringService;
this.statusService = statusService;
this.db = db;
}
testNotification = asyncHandler(
async (req, res, next) => {
testNotification = this.asyncHandler(
async (req, res) => {
const notification = req.body;
const success = await this.notificationService.sendTestNotification(notification);
if (!success) {
return res.error({
msg: "Sending notification failed",
status: 400,
});
throw this.errorService.createServerError("Sending notification failed");
}
return res.success({
@@ -32,8 +28,8 @@ class NotificationController {
"testNotification"
);
createNotification = asyncHandler(
async (req, res, next) => {
createNotification = this.asyncHandler(
async (req, res) => {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
@@ -42,12 +38,12 @@ class NotificationController {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const userId = req?.user?._id;
if (!userId) {
throw new Error("No user ID in request");
throw this.errorService.createBadRequestError("User ID is required");
}
body.userId = userId;
body.teamId = teamId;
@@ -62,11 +58,11 @@ class NotificationController {
"createNotification"
);
getNotificationsByTeamId = asyncHandler(
async (req, res, next) => {
getNotificationsByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const notifications = await this.db.getNotificationsByTeamId(teamId);
@@ -80,18 +76,16 @@ class NotificationController {
"getNotificationsByTeamId"
);
deleteNotification = asyncHandler(
async (req, res, next) => {
deleteNotification = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
await this.db.deleteNotificationById(req.params.id);
@@ -103,19 +97,17 @@ class NotificationController {
"deleteNotification"
);
getNotificationById = asyncHandler(
async (req, res, next) => {
getNotificationById = this.asyncHandler(
async (req, res) => {
const notification = await this.db.getNotificationById(req.params.id);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
if (!notification.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
return res.success({
msg: "Notification fetched successfully",
@@ -126,23 +118,21 @@ class NotificationController {
"getNotificationById"
);
editNotification = asyncHandler(
async (req, res, next) => {
editNotification = this.asyncHandler(
async (req, res) => {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
const editedNotification = await this.db.editNotification(req.params.id, req.body);
@@ -155,26 +145,24 @@ class NotificationController {
"editNotification"
);
testAllNotifications = asyncHandler(
async (req, res, next) => {
testAllNotifications = this.asyncHandler(
async (req, res) => {
const monitorId = req.body.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
const notifications = monitor.notifications;
if (notifications.length === 0) throw new Error("No notifications");
if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications");
const result = await this.notificationService.testAllNotifications(notifications);
if (!result) throw new Error("Failed to send all notifications");
if (!result) throw this.errorService.createServerError("Failed to send all notifications");
return res.success({
msg: "All notifications sent successfully",
});

View File

@@ -1,15 +1,14 @@
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "JobQueueController";
class JobQueueController {
constructor(jobQueue, stringService) {
class JobQueueController extends BaseController {
constructor(commonDependencies, { jobQueue }) {
super(commonDependencies);
this.jobQueue = jobQueue;
this.stringService = stringService;
}
getMetrics = asyncHandler(
async (req, res, next) => {
getMetrics = this.asyncHandler(
async (req, res) => {
const metrics = await this.jobQueue.getMetrics();
res.success({
msg: this.stringService.queueGetMetrics,
@@ -20,8 +19,8 @@ class JobQueueController {
"getMetrics"
);
getJobs = asyncHandler(
async (req, res, next) => {
getJobs = this.asyncHandler(
async (req, res) => {
const jobs = await this.jobQueue.getJobs();
return res.success({
msg: this.stringService.queueGetJobs,
@@ -32,8 +31,8 @@ class JobQueueController {
"getJobs"
);
getAllMetrics = asyncHandler(
async (req, res, next) => {
getAllMetrics = this.asyncHandler(
async (req, res) => {
const jobs = await this.jobQueue.getJobs();
const metrics = await this.jobQueue.getMetrics();
return res.success({
@@ -45,8 +44,8 @@ class JobQueueController {
"getAllMetrics"
);
addJob = asyncHandler(
async (req, res, next) => {
addJob = this.asyncHandler(
async (req, res) => {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.success({
msg: this.stringService.queueAddJob,
@@ -56,8 +55,8 @@ class JobQueueController {
"addJob"
);
flushQueue = asyncHandler(
async (req, res, next) => {
flushQueue = this.asyncHandler(
async (req, res) => {
const result = await this.jobQueue.flushQueues();
return res.success({
msg: this.stringService.jobQueueFlush,
@@ -68,8 +67,8 @@ class JobQueueController {
"flushQueue"
);
checkQueueHealth = asyncHandler(
async (req, res, next) => {
checkQueueHealth = this.asyncHandler(
async (req, res) => {
const stuckQueues = await this.jobQueue.checkQueueHealth();
return res.success({
msg: this.stringService.queueHealthCheck,

View File

@@ -1,14 +1,13 @@
import { updateAppSettingsBodyValidation } from "../validation/joi.js";
import { sendTestEmailBodyValidation } from "../validation/joi.js";
import { asyncHandler, createServerError } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "SettingsController";
class SettingsController {
constructor({ db, settingsService, stringService, emailService }) {
this.db = db;
class SettingsController extends BaseController {
constructor(commonDependencies, { settingsService, emailService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.emailService = emailService;
}
@@ -33,8 +32,8 @@ class SettingsController {
return returnSettings;
};
getAppSettings = asyncHandler(
async (req, res, next) => {
getAppSettings = this.asyncHandler(
async (req, res) => {
const dbSettings = await this.settingsService.getDBSettings();
const returnSettings = this.buildAppSettings(dbSettings);
@@ -47,8 +46,8 @@ class SettingsController {
"getAppSettings"
);
updateAppSettings = asyncHandler(
async (req, res, next) => {
updateAppSettings = this.asyncHandler(
async (req, res) => {
await updateAppSettingsBodyValidation.validateAsync(req.body);
const updatedSettings = await this.db.updateAppSettings(req.body);
@@ -62,14 +61,9 @@ class SettingsController {
"updateAppSettings"
);
sendTestEmail = asyncHandler(
async (req, res, next) => {
try {
await sendTestEmailBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
sendTestEmail = this.asyncHandler(
async (req, res) => {
await sendTestEmailBodyValidation.validateAsync(req.body);
const {
to,
@@ -107,7 +101,7 @@ class SettingsController {
});
if (!messageId) {
throw createServerError("Failed to send test email.");
throw this.errorService.createServerError("Failed to send test email.");
}
return res.success({

View File

@@ -1,16 +1,15 @@
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "statusPageController";
class StatusPageController {
constructor(db, stringService) {
this.db = db;
this.stringService = stringService;
class StatusPageController extends BaseController {
constructor(commonDependencies) {
super(commonDependencies);
}
createStatusPage = asyncHandler(
async (req, res, next) => {
createStatusPage = this.asyncHandler(
async (req, res) => {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
@@ -30,16 +29,14 @@ class StatusPageController {
"createStatusPage"
);
updateStatusPage = asyncHandler(
async (req, res, next) => {
updateStatusPage = this.asyncHandler(
async (req, res) => {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
const statusPage = await this.db.updateStatusPage(req.body, req.file);
if (statusPage === null) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound);
}
return res.success({
msg: this.stringService.statusPageUpdate,
@@ -50,8 +47,8 @@ class StatusPageController {
"updateStatusPage"
);
getStatusPage = asyncHandler(
async (req, res, next) => {
getStatusPage = this.asyncHandler(
async (req, res) => {
const statusPage = await this.db.getStatusPage();
return res.success({
msg: this.stringService.statusPageByUrl,
@@ -62,8 +59,8 @@ class StatusPageController {
"getStatusPage"
);
getStatusPageByUrl = asyncHandler(
async (req, res, next) => {
getStatusPageByUrl = this.asyncHandler(
async (req, res) => {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
@@ -77,8 +74,8 @@ class StatusPageController {
"getStatusPageByUrl"
);
getStatusPagesByTeamId = asyncHandler(
async (req, res, next) => {
getStatusPagesByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req.user.teamId;
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
@@ -91,8 +88,8 @@ class StatusPageController {
"getStatusPagesByTeamId"
);
deleteStatusPage = asyncHandler(
async (req, res, next) => {
deleteStatusPage = this.asyncHandler(
async (req, res) => {
await this.db.deleteStatusPage(req.params.url);
return res.success({
msg: this.stringService.statusPageDelete,

View File

@@ -3,16 +3,17 @@ import fs from "fs";
import swaggerUi from "swagger-ui-express";
import jwt from "jsonwebtoken";
import papaparse from "papaparse";
import express from "express";
import helmet from "helmet";
import cors from "cors";
import compression from "compression";
import { Logger } from "./utils/logger.js";
import logger from "./utils/logger.js";
import { verifyJWT } from "./middleware/verifyJWT.js";
import { handleErrors } from "./middleware/handleErrors.js";
import { responseHandler } from "./middleware/responseHandler.js";
import { fileURLToPath } from "url";
import { createCommonDependencies } from "./controllers/baseController.js";
import AuthRoutes from "./routes/authRoute.js";
import AuthController from "./controllers/authController.js";
@@ -58,6 +59,8 @@ import PulseQueueHelper from "./service/infrastructure/PulseQueue/PulseQueueHelp
import SuperSimpleQueue from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import SuperSimpleQueueHelper from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import ErrorService from "./service/infrastructure/errorService.js";
// Business services
import UserService from "./service/business/userService.js";
import CheckService from "./service/business/checkService.js";
@@ -186,6 +189,7 @@ const startApp = async () => {
});
const redisService = new RedisService({ Redis: IORedis, logger });
const errorService = new ErrorService();
// const jobQueueHelper = new JobQueueHelper({
// redisService,
@@ -240,11 +244,13 @@ const startApp = async () => {
logger,
stringService,
jwt,
errorService,
});
const checkService = new CheckService({
db,
settingsService,
stringService,
errorService,
});
const diagnosticService = new DiagnosticService();
const inviteService = new InviteService({
@@ -252,11 +258,13 @@ const startApp = async () => {
settingsService,
emailService,
stringService,
errorService,
});
const maintenanceWindowService = new MaintenanceWindowService({
db,
settingsService,
stringService,
errorService,
});
const monitorService = new MonitorService({
db,
@@ -266,11 +274,14 @@ const startApp = async () => {
emailService,
papaparse,
logger,
errorService,
});
// Register services
// ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
// ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue);
ServiceRegistry.register(Logger.SERVICE_NAME, logger);
ServiceRegistry.register(JobQueue.SERVICE_NAME, superSimpleQueue);
ServiceRegistry.register(ErrorService.SERVICE_NAME, errorService);
ServiceRegistry.register(MongoDB.SERVICE_NAME, db);
ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService);
ServiceRegistry.register(EmailService.SERVICE_NAME, emailService);
@@ -285,6 +296,7 @@ const startApp = async () => {
ServiceRegistry.register(InviteService.SERVICE_NAME, inviteService);
ServiceRegistry.register(MaintenanceWindowService.SERVICE_NAME, maintenanceWindowService);
ServiceRegistry.register(MonitorService.SERVICE_NAME, monitorService);
ServiceRegistry.register(DiagnosticService.SERVICE_NAME, diagnosticService);
await translationService.initialize();
@@ -298,66 +310,63 @@ const startApp = async () => {
process.on("SIGTERM", shutdown);
//Create controllers
const authController = new AuthController({
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
const commonDependencies = createCommonDependencies(
ServiceRegistry,
MongoDB.SERVICE_NAME,
Logger.SERVICE_NAME,
ErrorService.SERVICE_NAME,
StringService.SERVICE_NAME
);
const authController = new AuthController(commonDependencies, {
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
emailService: ServiceRegistry.get(EmailService.SERVICE_NAME),
jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
logger: logger,
userService: ServiceRegistry.get(UserService.SERVICE_NAME),
});
const monitorController = new MonitorController({
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
const monitorController = new MonitorController(commonDependencies, {
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
emailService: ServiceRegistry.get(EmailService.SERVICE_NAME),
monitorService: ServiceRegistry.get(MonitorService.SERVICE_NAME),
});
const settingsController = new SettingsController({
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
const settingsController = new SettingsController(commonDependencies, {
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
emailService: ServiceRegistry.get(EmailService.SERVICE_NAME),
});
const checkController = new CheckController({
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
const checkController = new CheckController(commonDependencies, {
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
checkService: ServiceRegistry.get(CheckService.SERVICE_NAME),
});
const inviteController = new InviteController({
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
inviteService,
const inviteController = new InviteController(commonDependencies, {
inviteService: ServiceRegistry.get(InviteService.SERVICE_NAME),
});
const maintenanceWindowController = new MaintenanceWindowController({
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
const maintenanceWindowController = new MaintenanceWindowController(commonDependencies, {
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
maintenanceWindowService: ServiceRegistry.get(MaintenanceWindowService.SERVICE_NAME),
});
const queueController = new QueueController(ServiceRegistry.get(JobQueue.SERVICE_NAME), ServiceRegistry.get(StringService.SERVICE_NAME));
const logController = new LogController(logger);
const statusPageController = new StatusPageController(ServiceRegistry.get(MongoDB.SERVICE_NAME), ServiceRegistry.get(StringService.SERVICE_NAME));
const notificationController = new NotificationController({
notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
statusService: ServiceRegistry.get(StatusService.SERVICE_NAME),
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
const queueController = new QueueController(commonDependencies, {
jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME),
});
const diagnosticController = new DiagnosticController({
diagnosticService,
const logController = new LogController(commonDependencies);
const statusPageController = new StatusPageController(commonDependencies);
const notificationController = new NotificationController(commonDependencies, {
notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME),
statusService: ServiceRegistry.get(StatusService.SERVICE_NAME),
});
const diagnosticController = new DiagnosticController(commonDependencies, {
diagnosticService: ServiceRegistry.get(DiagnosticService.SERVICE_NAME),
});
//Create routes
@@ -370,9 +379,9 @@ const startApp = async () => {
const queueRoutes = new QueueRoutes(queueController);
const logRoutes = new LogRoutes(logController);
const statusPageRoutes = new StatusPageRoutes(statusPageController);
const notificationRoutes = new NotificationRoutes(notificationController);
const diagnosticRoutes = new DiagnosticRoutes(diagnosticController);
// Middleware
app.use(express.static(frontendPath));
app.use(responseHandler);
@@ -415,16 +424,16 @@ const startApp = async () => {
//routes
app.use("/api/v1/auth", authRoutes.getRouter());
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
app.use("/api/v1/invite", inviteRoutes.getRouter());
app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter());
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
app.use("/api/v1/invite", inviteRoutes.getRouter());
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
app.use("/api/v1/health", (req, res) => {
res.json({

View File

@@ -3,7 +3,6 @@ import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
const handleErrors = (error, req, res, next) => {
console.log("ERROR", error);
const status = error.status || 500;
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
const message = error.message || stringService.authIncorrectPassword;

View File

@@ -3,37 +3,30 @@ const SERVICE_NAME = "checkService";
class CheckService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, stringService }) {
constructor({ db, settingsService, stringService, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.stringService = stringService;
this.errorService = errorService;
}
getChecksByMonitor = async ({ monitorId, query, teamId }) => {
if (!monitorId) {
throw new Error("No monitor ID in request");
throw this.errorService.createBadRequestError("No monitor ID in request");
}
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor) {
const err = new Error("Monitor not found");
err.status = 404;
err.service = SERVICE_NAME;
err.method = "getChecksByMonitor";
throw err;
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
const err = new Error("Unauthorized");
err.status = 403;
err.service = SERVICE_NAME;
err.method = "getChecksByMonitor";
throw err;
throw this.errorService.createAuthorizationError();
}
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
@@ -55,7 +48,7 @@ class CheckService {
let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const checkData = await this.db.getChecksByTeam({
@@ -72,7 +65,7 @@ class CheckService {
getChecksSummaryByTeamId = async ({ teamId }) => {
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
@@ -81,11 +74,11 @@ class CheckService {
ackCheck = async ({ checkId, teamId, ack }) => {
if (!checkId) {
throw new Error("No check ID in request");
throw this.errorService.createBadRequestError("No check ID in request");
}
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
@@ -95,20 +88,16 @@ class CheckService {
ackAllChecks = async ({ monitorId, path, teamId, ack }) => {
if (path === "monitor") {
if (!monitorId) {
throw new Error("No monitor ID in request");
throw this.errorService.createBadRequestError("No monitor ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor) {
throw new Error("Monitor not found");
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
const err = new Error("Unauthorized");
err.status = 403;
err.service = SERVICE_NAME;
err.method = "ackAllChecks";
throw err;
throw this.errorService.createAuthorizationError();
}
}
@@ -118,29 +107,21 @@ class CheckService {
deleteChecks = async ({ monitorId, teamId }) => {
if (!monitorId) {
throw new Error("No monitor ID in request");
throw this.errorService.createBadRequestError("No monitor ID in request");
}
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor) {
const err = new Error("Monitor not found");
err.status = 404;
err.service = SERVICE_NAME;
err.method = "deleteChecks";
throw err;
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
const err = new Error("Unauthorized");
err.status = 403;
err.service = SERVICE_NAME;
err.method = "deleteChecks";
throw err;
throw this.errorService.createAuthorizationError();
}
const deletedCount = await this.db.deleteChecks(monitorId);
@@ -148,7 +129,7 @@ class CheckService {
};
deleteChecksByTeamId = async ({ teamId }) => {
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const deletedCount = await this.db.deleteChecksByTeamId(teamId);

View File

@@ -1,14 +1,14 @@
const SERVICE_NAME = "inviteService";
import { createServerError } from "../../utils/errorUtils.js";
class InviteService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, emailService, stringService }) {
constructor({ db, settingsService, emailService, stringService, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
this.stringService = stringService;
this.errorService = errorService;
}
getInviteToken = async ({ invite, teamId }) => {
@@ -27,7 +27,7 @@ class InviteService {
});
const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html);
if (!result) {
throw createServerError("Failed to send invite e-mail... Please verify your settings.");
throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings.");
}
};

View File

@@ -2,10 +2,11 @@ const SERVICE_NAME = "maintenanceWindowService";
class MaintenanceWindowService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, stringService }) {
constructor({ db, settingsService, stringService, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.stringService = stringService;
this.errorService = errorService;
}
createMaintenanceWindow = async ({ teamId, body }) => {
@@ -15,11 +16,7 @@ class MaintenanceWindowService {
const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId));
if (unauthorizedMonitors.length > 0) {
const error = new Error("Unauthorized access to one or more monitors");
error.status = 403;
error.service = SERVICE_NAME;
error.method = "createMaintenanceWindows";
throw error;
throw this.errorService.createAuthorizationError();
}
const dbTransactions = monitorIds.map((monitorId) => {

View File

@@ -1,12 +1,10 @@
import { createMonitorsBodyValidation } from "../../validation/joi.js";
import { createServerError } from "../../utils/errorUtils.js";
import logger from "../../utils/logger.js";
const SERVICE_NAME = "MonitorService";
class MonitorService {
SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse }) {
constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
@@ -14,14 +12,13 @@ class MonitorService {
this.emailService = emailService;
this.papaparse = papaparse;
this.logger = logger;
this.errorService = errorService;
}
verifyTeamAccess = async ({ teamId, monitorId }) => {
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor?.teamId?.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
};
@@ -93,7 +90,7 @@ class MonitorService {
if (["port", "interval"].includes(header)) {
const num = parseInt(value, 10);
if (isNaN(num)) {
throw new Error(`${header} should be a valid number, got: ${value}`);
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
}
return num;
}
@@ -103,11 +100,11 @@ class MonitorService {
complete: async ({ data, errors }) => {
try {
if (errors.length > 0) {
throw createServerError("Error parsing CSV");
throw this.errorService.createServerError("Error parsing CSV");
}
if (!data || data.length === 0) {
throw createServerError("CSV file contains no data rows");
throw this.errorService.createServerError("CSV file contains no data rows");
}
const enrichedData = data.map((monitor) => ({
@@ -156,7 +153,7 @@ class MonitorService {
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.warn({
this.logger.warn({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
@@ -195,7 +192,7 @@ class MonitorService {
const messageId = await this.emailService.sendEmail(to, subject, html);
if (!messageId) {
throw createServerError("Failed to send test email.");
throw this.errorService.createServerError("Failed to send test email.");
}
return messageId;
@@ -240,10 +237,10 @@ class MonitorService {
};
exportMonitorsToCSV = async ({ teamId }) => {
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId });
const monitors = await this.db.getMonitorsByTeamId({ teamId });
if (!monitors || monitors.length === 0) {
throw new Error("No monitors to export");
throw this.errorService.createNotFoundError("No monitors to export");
}
const csvData = monitors?.filteredMonitors?.map((monitor) => ({

View File

@@ -1,15 +1,15 @@
const SERVICE_NAME = "userService";
import { createAuthError, createError } from "../../utils/errorUtils.js";
class UserService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, emailService, settingsService, logger, stringService, jwt }) {
constructor({ db, emailService, settingsService, logger, stringService, jwt, errorService }) {
this.db = db;
this.emailService = emailService;
this.settingsService = settingsService;
this.logger = logger;
this.stringService = stringService;
this.jwt = jwt;
this.errorService = errorService;
}
issueToken = (payload, appSettings) => {
@@ -80,7 +80,7 @@ class UserService {
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
throw createAuthError(this.stringService.authIncorrectPassword);
throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword);
}
// Remove password from user object. Should this be abstracted to DB layer?
@@ -109,7 +109,7 @@ class UserService {
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
throw createError(this.stringService.authIncorrectPassword, 403);
throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword);
}
// If a match, update the password
updates.password = updates.newPassword;
@@ -154,23 +154,23 @@ class UserService {
deleteUser = async (user) => {
const email = user?.email;
if (!email) {
throw new Error("No email in request");
throw this.errorService.createBadRequestError("No email in request");
}
const teamId = user?.teamId;
const userId = user?._id;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
if (!userId) {
throw new Error("No user ID in request");
throw this.errorService.createBadRequestError("No user ID in request");
}
const roles = user?.role;
if (roles.includes("demo")) {
throw new Error("Demo user cannot be deleted");
throw this.errorService.createBadRequestError("Demo user cannot be deleted");
}
// 1. Find all the monitors associated with the team ID if superadmin

View File

@@ -0,0 +1,98 @@
export class AppError extends Error {
constructor(message, status = 500, service = null, method = null, details = null) {
super(message);
this.status = status;
this.service = service;
this.method = method;
this.details = details;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 422, service, method, details);
}
}
class AuthenticationError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 401, service, method, details);
}
}
class AuthorizationError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 403, service, method, details);
}
}
class NotFoundError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 404, service, method, details);
}
}
class ConflictError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 409, service, method, details);
}
}
class DatabaseError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 500, service, method, details);
}
}
class BadRequestError extends AppError {
constructor(message, details = null, service = null, method = null) {
super(message, 400, service, method, details);
}
}
const SERVICE_NAME = "ErrorService";
class ErrorService {
static SERVICE_NAME = SERVICE_NAME;
constructor() {}
createError = (message, status = 500, service = null, method = null, details = null) => {
return new AppError(message, status, service, method, details);
};
createValidationError = (message, details = null, service = null, method = null) => {
return new ValidationError(message, details, service, method);
};
createAuthenticationError = (message = "Unauthorized", details = null, service = null, method = null) => {
return new AuthenticationError(message, details, service, method);
};
createAuthorizationError = (message, details = null, service = null, method = null) => {
return new AuthorizationError(message, details, service, method);
};
createNotFoundError = (message, details = null, service = null, method = null) => {
return new NotFoundError(message, details, service, method);
};
createConflictError = (message, details = null, service = null, method = null) => {
return new ConflictError(message, details, service, method);
};
createDatabaseError = (message, details = null, service = null, method = null) => {
return new DatabaseError(message, details, service, method);
};
createServerError = (message, details = null, service = null, method = null) => {
return this.createError(message, 500, service, method, details);
};
createBadRequestError = (message = "BadRequest", details = null, service = null, method = null) => {
return new BadRequestError(message, details, service, method);
};
}
export default ErrorService;

View File

@@ -1,81 +0,0 @@
class AppError extends Error {
constructor(message, status = 500, service = null, method = null, details = null) {
super(message);
this.status = status;
this.service = service;
this.method = method;
this.details = details;
Error.captureStackTrace(this, this.constructor);
}
}
export const createError = (message, status = 500, service = null, method = null, details = null) => {
return new AppError(message, status, service, method, details);
};
export const createValidationError = (message, details = null, service = null, method = null) => {
return createError(message, 422, service, method, details);
};
export const createAuthError = (message, details = null, service = null, method = null) => {
return createError(message, 401, service, method, details);
};
export const createForbiddenError = (message, details = null, service = null, method = null) => {
return createError(message, 403, service, method, details);
};
export const createNotFoundError = (message, details = null, service = null, method = null) => {
return createError(message, 404, service, method, details);
};
export const createConflictError = (message, details = null, service = null, method = null) => {
return createError(message, 409, service, method, details);
};
export const createServerError = (message, details = null, service = null, method = null) => {
return createError(message, 500, service, method, details);
};
export const asyncHandler = (fn, serviceName, methodName) => {
return async (req, res, next) => {
try {
await fn(req, res, next);
} catch (error) {
// Handle validation errors
if (error.isJoi || error.name === "ValidationError") {
const validationError = createValidationError(error.message, error.details, serviceName, methodName);
return next(validationError);
}
if (error instanceof AppError) {
error.service = error.service || serviceName;
error.method = error.method || methodName;
return next(error);
}
if (error.code === "23505") {
const appError = createConflictError("Resource already exists", {
originalError: error.message,
code: error.code,
});
appError.service = serviceName;
appError.method = methodName;
return next(appError);
}
if (error.status) {
const appError = createError(error.message, error.status, serviceName, methodName, { originalError: error.message, stack: error.stack });
return next(appError);
}
// For unknown errors, create a server error
const appError = createServerError(error.message || "An unexpected error occurred", { originalError: error.message, stack: error.stack });
appError.service = serviceName;
appError.method = methodName;
appError.stack = error.stack; // Preserve original stack
return next(appError);
}
};
};

View File

@@ -2,7 +2,10 @@ import { createLogger, format, transports } from "winston";
import dotenv from "dotenv";
dotenv.config();
const SERVICE_NAME = "Logger";
class Logger {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
this.logCache = [];
this.maxCacheSize = 1000;