diff --git a/server/app.js b/server/app.js new file mode 100644 index 000000000..af53dbf56 --- /dev/null +++ b/server/app.js @@ -0,0 +1,73 @@ +import express from "express"; +import path from "path"; +import { responseHandler } from "./middleware/responseHandler.js"; +import cors from "cors"; +import helmet from "helmet"; +import compression from "compression"; +import languageMiddleware from "./middleware/languageMiddleware.js"; +import swaggerUi from "swagger-ui-express"; +import { handleErrors } from "./middleware/handleErrors.js"; +import { setupRoutes } from "./config/routes.js"; +export const createApp = ({ services, controllers, appSettings, frontendPath, openApiSpec }) => { + const allowedOrigin = appSettings.clientHost; + + const app = express(); + + // Static files + app.use(express.static(frontendPath)); + + // Response handler + app.use(responseHandler); + + app.use( + cors({ + origin: allowedOrigin, + methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", + allowedHeaders: "*", + credentials: true, + }) + ); + app.use(express.json()); + app.use( + helmet({ + hsts: false, + contentSecurityPolicy: { + useDefaults: true, + directives: { + upgradeInsecureRequests: null, + }, + }, + }) + ); + app.use( + compression({ + level: 6, + threshold: 1024, + filter: (req, res) => { + if (req.headers["x-no-compression"]) { + return false; + } + return compression.filter(req, res); + }, + }) + ); + app.use(languageMiddleware(services.stringService, services.translationService, services.settingsService)); + // Swagger UI + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); + + app.use("/api/v1/health", (req, res) => { + res.json({ + status: "OK", + }); + }); + + // Main app routes + setupRoutes(app, controllers); + + // FE routes + app.get("*", (req, res) => { + res.sendFile(path.join(frontendPath, "index.html")); + }); + app.use(handleErrors); + return app; +}; diff --git a/server/config/controllers.js b/server/config/controllers.js new file mode 100644 index 000000000..2185f9ffd --- /dev/null +++ b/server/config/controllers.js @@ -0,0 +1,66 @@ +import { createCommonDependencies } from "../controllers/baseController.js"; + +// Services + +// Controllers +import MonitorController from "../controllers/monitorController.js"; +import AuthController from "../controllers/authController.js"; +import SettingsController from "../controllers/settingsController.js"; +import CheckController from "../controllers/checkController.js"; +import InviteController from "../controllers/inviteController.js"; +import MaintenanceWindowController from "../controllers/maintenanceWindowController.js"; +import QueueController from "../controllers/queueController.js"; +import LogController from "../controllers/logController.js"; +import StatusPageController from "../controllers/statusPageController.js"; +import NotificationController from "../controllers/notificationController.js"; +import DiagnosticController from "../controllers/diagnosticController.js"; + +export const initializeControllers = (services) => { + const controllers = {}; + const commonDependencies = createCommonDependencies(services.db, services.logger, services.errorService, services.stringService); + + controllers.authController = new AuthController(commonDependencies, { + settingsService: services.settingsService, + emailService: services.emailService, + jobQueue: services.jobQueue, + userService: services.userService, + }); + + controllers.monitorController = new MonitorController(commonDependencies, { + settingsService: services.settingsService, + jobQueue: services.jobQueue, + emailService: services.emailService, + monitorService: services.monitorService, + }); + + controllers.settingsController = new SettingsController(commonDependencies, { + settingsService: services.settingsService, + emailService: services.emailService, + }); + controllers.checkController = new CheckController(commonDependencies, { + settingsService: services.settingsService, + checkService: services.checkService, + }); + controllers.inviteController = new InviteController(commonDependencies, { + inviteService: services.inviteService, + }); + + controllers.maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { + settingsService: services.settingsService, + maintenanceWindowService: services.maintenanceWindowService, + }); + controllers.queueController = new QueueController(commonDependencies, { + jobQueue: services.jobQueue, + }); + controllers.logController = new LogController(commonDependencies); + controllers.statusPageController = new StatusPageController(commonDependencies); + controllers.notificationController = new NotificationController(commonDependencies, { + notificationService: services.notificationService, + statusService: services.statusService, + }); + controllers.diagnosticController = new DiagnosticController(commonDependencies, { + diagnosticService: services.diagnosticService, + }); + + return controllers; +}; diff --git a/server/config/database.js b/server/config/database.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/config/routes.js b/server/config/routes.js new file mode 100644 index 000000000..64b1df474 --- /dev/null +++ b/server/config/routes.js @@ -0,0 +1,39 @@ +import { verifyJWT } from "../middleware/verifyJWT.js"; + +import AuthRoutes from "../routes/authRoute.js"; +import InviteRoutes from "../routes/inviteRoute.js"; +import MonitorRoutes from "../routes/monitorRoute.js"; +import CheckRoutes from "../routes/checkRoute.js"; +import SettingsRoutes from "../routes/settingsRoute.js"; +import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js"; +import StatusPageRoutes from "../routes/statusPageRoute.js"; +import QueueRoutes from "../routes/queueRoute.js"; +import LogRoutes from "../routes/logRoutes.js"; +import DiagnosticRoutes from "../routes/diagnosticRoute.js"; +import NotificationRoutes from "../routes/notificationRoute.js"; + +export const setupRoutes = (app, controllers) => { + const authRoutes = new AuthRoutes(controllers.authController); + const monitorRoutes = new MonitorRoutes(controllers.monitorController); + const settingsRoutes = new SettingsRoutes(controllers.settingsController); + const checkRoutes = new CheckRoutes(controllers.checkController); + const inviteRoutes = new InviteRoutes(controllers.inviteController); + const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController); + const queueRoutes = new QueueRoutes(controllers.queueController); + const logRoutes = new LogRoutes(controllers.logController); + const statusPageRoutes = new StatusPageRoutes(controllers.statusPageController); + const notificationRoutes = new NotificationRoutes(controllers.notificationController); + const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController); + + app.use("/api/v1/auth", authRoutes.getRouter()); + app.use("/api/v1/monitors", verifyJWT, monitorRoutes.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()); +}; diff --git a/server/config/services.js b/server/config/services.js new file mode 100644 index 000000000..490c252b9 --- /dev/null +++ b/server/config/services.js @@ -0,0 +1,152 @@ +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import logger from "../utils/logger.js"; +import TranslationService from "../service/system/translationService.js"; +import StringService from "../service/system/stringService.js"; +import MongoDB from "../db/mongo/MongoDB.js"; +import NetworkService from "../service/infrastructure/networkService.js"; +import EmailService from "../service/infrastructure/emailService.js"; +import BufferService from "../service/infrastructure/bufferService.js"; +import StatusService from "../service/infrastructure/statusService.js"; +import NotificationUtils from "../service/infrastructure/notificationUtils.js"; +import NotificationService from "../service/infrastructure/notificationService.js"; +import RedisService from "../service/data/redisService.js"; +import ErrorService from "../service/infrastructure/errorService.js"; +import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import UserService from "../service/business/userService.js"; +import CheckService from "../service/business/checkService.js"; +import DiagnosticService from "../service/business/diagnosticService.js"; +import InviteService from "../service/business/inviteService.js"; +import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; +import MonitorService from "../service/business/monitorService.js"; +import IORedis from "ioredis"; +import papaparse from "papaparse"; +import axios from "axios"; +import ping from "ping"; +import http from "http"; +import Docker from "dockerode"; +import net from "net"; +import fs from "fs"; +import path from "path"; +import nodemailer from "nodemailer"; +import pkg from "handlebars"; +const { compile } = pkg; +import mjml2html from "mjml"; +import jwt from "jsonwebtoken"; + +export const initializeServices = async (appSettings, settingsService) => { + const translationService = new TranslationService(logger); + await translationService.initialize(); + + const stringService = new StringService(translationService); + + // Create DB + const db = new MongoDB({ appSettings }); + await db.connect(); + + const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService); + const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); + const bufferService = new BufferService({ db, logger }); + const statusService = new StatusService({ db, logger, buffer: bufferService }); + + const notificationUtils = new NotificationUtils({ + stringService, + emailService, + }); + + const notificationService = new NotificationService({ + emailService, + db, + logger, + networkService, + stringService, + notificationUtils, + }); + + const redisService = new RedisService({ Redis: IORedis, logger }); + const errorService = new ErrorService(); + + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ + db, + logger, + networkService, + statusService, + notificationService, + }); + + const superSimpleQueue = await SuperSimpleQueue.create({ + appSettings, + db, + logger, + helper: superSimpleQueueHelper, + }); + + // Business services + const userService = new UserService({ + db, + emailService, + settingsService, + logger, + stringService, + jwt, + errorService, + }); + const checkService = new CheckService({ + db, + settingsService, + stringService, + errorService, + }); + const diagnosticService = new DiagnosticService(); + const inviteService = new InviteService({ + db, + settingsService, + emailService, + stringService, + errorService, + }); + const maintenanceWindowService = new MaintenanceWindowService({ + db, + settingsService, + stringService, + errorService, + }); + const monitorService = new MonitorService({ + db, + settingsService, + jobQueue: superSimpleQueue, + stringService, + emailService, + papaparse, + logger, + errorService, + }); + + const services = { + settingsService, + translationService, + stringService, + db, + networkService, + emailService, + bufferService, + statusService, + notificationService, + redisService, + jobQueue: superSimpleQueue, + userService, + checkService, + diagnosticService, + inviteService, + maintenanceWindowService, + monitorService, + errorService, + logger, + }; + + Object.values(services).forEach((service) => { + ServiceRegistry.register(service.serviceName, service); + }); + + return services; +}; diff --git a/server/controllers/announcementsController.js b/server/controllers/announcementsController.js index aeb7a3b9d..430859791 100755 --- a/server/controllers/announcementsController.js +++ b/server/controllers/announcementsController.js @@ -1,4 +1,5 @@ import { createAnnouncementValidation } from "../validation/joi.js"; +import BaseController from "./baseController.js"; const SERVICE_NAME = "announcementController"; @@ -10,12 +11,17 @@ const SERVICE_NAME = "announcementController"; */ class AnnouncementController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies) { super(commonDependencies); this.createAnnouncement = this.createAnnouncement.bind(this); this.getAnnouncement = this.getAnnouncement.bind(this); } + get serviceName() { + return AnnouncementController.SERVICE_NAME; + } + /** * Handles the creation of a new announcement. * diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 00fc2caf5..c186e8423 100755 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -24,6 +24,7 @@ const SERVICE_NAME = "authController"; * @description Manages user authentication and authorization operations */ class AuthController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; /** * Creates an instance of AuthController. * @@ -42,6 +43,10 @@ class AuthController extends BaseController { this.userService = userService; } + get serviceName() { + return AuthController.SERVICE_NAME; + } + /** * Registers a new user in the system. * diff --git a/server/controllers/baseController.js b/server/controllers/baseController.js index a84806a24..910cf57b4 100644 --- a/server/controllers/baseController.js +++ b/server/controllers/baseController.js @@ -1,11 +1,11 @@ import { AppError } from "../service/infrastructure/errorService.js"; -export const createCommonDependencies = (serviceRegistry, dbServiceName, loggerServiceName, errorServiceName, stringServiceName) => { +export const createCommonDependencies = (db, errorService, logger, stringService) => { return { - db: serviceRegistry.get(dbServiceName), - errorService: serviceRegistry.get(errorServiceName), - logger: serviceRegistry.get(loggerServiceName), - stringService: serviceRegistry.get(stringServiceName), + db, + errorService, + logger, + stringService, }; }; diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js index 08b6c2de9..4069df3ff 100755 --- a/server/controllers/checkController.js +++ b/server/controllers/checkController.js @@ -23,6 +23,7 @@ const SERVICE_NAME = "checkController"; * @description Manages check operations and monitoring data */ class CheckController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; /** * Creates an instance of CheckController. * @@ -37,6 +38,10 @@ class CheckController extends BaseController { this.checkService = checkService; } + get serviceName() { + return CheckController.SERVICE_NAME; + } + /** * Retrieves checks for a specific monitor with filtering and pagination. * diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index 91b90f96b..0723a9f6c 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -10,6 +10,7 @@ import BaseController from "./baseController.js"; * @description Manages system diagnostics and performance monitoring */ class DiagnosticController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; /** * Creates an instance of DiagnosticController. * @param {Object} commonDependencies - Common dependencies injected into the controller @@ -21,6 +22,9 @@ class DiagnosticController extends BaseController { this.diagnosticService = diagnosticService; } + get serviceName() { + return DiagnosticController.SERVICE_NAME; + } /** * Retrieves comprehensive system statistics and performance metrics. * diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js index 3f237c6ec..a377234c0 100755 --- a/server/controllers/inviteController.js +++ b/server/controllers/inviteController.js @@ -7,6 +7,7 @@ const SERVICE_NAME = "inviteController"; * Manages invite token generation, email sending, and token verification */ class InviteController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; /** * Creates a new InviteController instance * @param {Object} commonDependencies - Common dependencies injected into the controller @@ -17,6 +18,10 @@ class InviteController extends BaseController { this.inviteService = inviteService; } + get serviceName() { + return InviteController.SERVICE_NAME; + } + /** * Generates an invite token for a user invitation * @param {Object} req - Express request object diff --git a/server/controllers/logController.js b/server/controllers/logController.js index aa2780eb6..3537246bb 100644 --- a/server/controllers/logController.js +++ b/server/controllers/logController.js @@ -2,10 +2,15 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "LogController"; class LogController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies) { super(commonDependencies); } + get serviceName() { + return LogController.SERVICE_NAME; + } + getLogs = this.asyncHandler( async (req, res) => { const logs = await this.logger.getLogs(); diff --git a/server/controllers/maintenanceWindowController.js b/server/controllers/maintenanceWindowController.js index e54726ea3..2fdb1ee15 100755 --- a/server/controllers/maintenanceWindowController.js +++ b/server/controllers/maintenanceWindowController.js @@ -12,12 +12,17 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "maintenanceWindowController"; class MaintenanceWindowController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies, { settingsService, maintenanceWindowService }) { super(commonDependencies); this.settingsService = settingsService; this.maintenanceWindowService = maintenanceWindowService; } + get serviceName() { + return MaintenanceWindowController.SERVICE_NAME; + } + createMaintenanceWindows = this.asyncHandler( async (req, res) => { await createMaintenanceWindowBodyValidation.validateAsync(req.body); diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js index 2187df5d0..13890e5ea 100755 --- a/server/controllers/monitorController.js +++ b/server/controllers/monitorController.js @@ -18,6 +18,7 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "monitorController"; class MonitorController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) { super(commonDependencies); this.settingsService = settingsService; @@ -26,6 +27,10 @@ class MonitorController extends BaseController { this.monitorService = monitorService; } + get serviceName() { + return MonitorController.SERVICE_NAME; + } + async verifyTeamAccess(teamId, monitorId) { const monitor = await this.db.getMonitorById(monitorId); if (!monitor.teamId.equals(teamId)) { diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index 6e85b97b8..081968e3d 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -4,12 +4,17 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "NotificationController"; class NotificationController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies, { notificationService, statusService }) { super(commonDependencies); this.notificationService = notificationService; this.statusService = statusService; } + get serviceName() { + return NotificationController.SERVICE_NAME; + } + testNotification = this.asyncHandler( async (req, res) => { const notification = req.body; diff --git a/server/controllers/queueController.js b/server/controllers/queueController.js index 757350652..d2c3fd06f 100755 --- a/server/controllers/queueController.js +++ b/server/controllers/queueController.js @@ -2,11 +2,16 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "JobQueueController"; class JobQueueController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies, { jobQueue }) { super(commonDependencies); this.jobQueue = jobQueue; } + get serviceName() { + return JobQueueController.SERVICE_NAME; + } + getMetrics = this.asyncHandler( async (req, res) => { const metrics = await this.jobQueue.getMetrics(); diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index d97c19e88..c66024025 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -5,12 +5,17 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "SettingsController"; class SettingsController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies, { settingsService, emailService }) { super(commonDependencies); this.settingsService = settingsService; this.emailService = emailService; } + get serviceName() { + return SettingsController.SERVICE_NAME; + } + buildAppSettings = (dbSettings) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; diff --git a/server/controllers/statusPageController.js b/server/controllers/statusPageController.js index 09d1d8b5f..85211c7ff 100755 --- a/server/controllers/statusPageController.js +++ b/server/controllers/statusPageController.js @@ -4,10 +4,15 @@ import BaseController from "./baseController.js"; const SERVICE_NAME = "statusPageController"; class StatusPageController extends BaseController { + static SERVICE_NAME = SERVICE_NAME; constructor(commonDependencies) { super(commonDependencies); } + get serviceName() { + return StatusPageController.SERVICE_NAME; + } + createStatusPage = this.asyncHandler( async (req, res) => { await createStatusPageBodyValidation.validateAsync(req.body); diff --git a/server/db/mongo/MongoDB.js b/server/db/mongo/MongoDB.js index 6eb82384f..e063214fd 100755 --- a/server/db/mongo/MongoDB.js +++ b/server/db/mongo/MongoDB.js @@ -1,5 +1,4 @@ import mongoose from "mongoose"; -import UserModel from "../models/User.js"; import AppSettings from "../models/AppSettings.js"; import logger from "../../utils/logger.js"; @@ -87,6 +86,10 @@ class MongoDB { Object.assign(this, diagnosticModule); } + get serviceName() { + return MongoDB.SERVICE_NAME; + } + connect = async () => { try { const connectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; diff --git a/server/index.js b/server/index.js index b6f1dd3fe..607847e8b 100755 --- a/server/index.js +++ b/server/index.js @@ -1,450 +1,47 @@ +import { initializeServices } from "./config/services.js"; +import { initializeControllers } from "./config/controllers.js"; +import { createApp } from "./app.js"; +import { initShutdownListener } from "./shutdown.js"; +import logger from "./utils/logger.js"; +import { fileURLToPath } from "url"; import path from "path"; 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"; - -import InviteRoutes from "./routes/inviteRoute.js"; -import InviteController from "./controllers/inviteController.js"; - -import MonitorRoutes from "./routes/monitorRoute.js"; -import MonitorController from "./controllers/monitorController.js"; - -import CheckRoutes from "./routes/checkRoute.js"; -import CheckController from "./controllers/checkController.js"; - -import MaintenanceWindowRoutes from "./routes/maintenanceWindowRoute.js"; -import MaintenanceWindowController from "./controllers/maintenanceWindowController.js"; - -import SettingsRoutes from "./routes/settingsRoute.js"; -import SettingsController from "./controllers/settingsController.js"; - -import StatusPageRoutes from "./routes/statusPageRoute.js"; -import StatusPageController from "./controllers/statusPageController.js"; - -import QueueRoutes from "./routes/queueRoute.js"; -import QueueController from "./controllers/queueController.js"; - -import LogRoutes from "./routes/logRoutes.js"; -import LogController from "./controllers/logController.js"; - -import NotificationRoutes from "./routes/notificationRoute.js"; -import NotificationController from "./controllers/notificationController.js"; - -import DiagnosticRoutes from "./routes/diagnosticRoute.js"; -import DiagnosticController from "./controllers/diagnosticController.js"; - -//JobQueue service and dependencies -import JobQueue from "./service/infrastructure/JobQueue/JobQueue.js"; -import JobQueueHelper from "./service/infrastructure/JobQueue/JobQueueHelper.js"; -import { Queue, Worker } from "bullmq"; - -import PulseQueue from "./service/infrastructure/PulseQueue/PulseQueue.js"; -import PulseQueueHelper from "./service/infrastructure/PulseQueue/PulseQueueHelper.js"; - -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"; -import DiagnosticService from "./service/business/diagnosticService.js"; -import InviteService from "./service/business/inviteService.js"; -import MaintenanceWindowService from "./service/business/maintenanceWindowService.js"; -import MonitorService from "./service/business/monitorService.js"; - -//Network service and dependencies -import NetworkService from "./service/infrastructure/networkService.js"; -import axios from "axios"; -import ping from "ping"; -import http from "http"; -import Docker from "dockerode"; -import net from "net"; -// Email service and dependencies -import EmailService from "./service/infrastructure/emailService.js"; -import nodemailer from "nodemailer"; -import pkg from "handlebars"; -const { compile } = pkg; -import mjml2html from "mjml"; - -// Settings Service and dependencies import SettingsService from "./service/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; -// Status Service and dependencies -import StatusService from "./service/infrastructure/statusService.js"; - -// Notification Service and dependencies -import NotificationService from "./service/infrastructure/notificationService.js"; -import NotificationUtils from "./service/infrastructure/notificationUtils.js"; - -// Buffer Service and dependencies -import BufferService from "./service/infrastructure/bufferService.js"; - -// Service Registry -import ServiceRegistry from "./service/system/serviceRegistry.js"; - -import MongoDB from "./db/mongo/MongoDB.js"; - -// Redis Service and dependencies -import IORedis from "ioredis"; -import RedisService from "./service/data/redisService.js"; - -import TranslationService from "./service/system/translationService.js"; -import languageMiddleware from "./middleware/languageMiddleware.js"; -import StringService from "./service/system/stringService.js"; - const SERVICE_NAME = "Server"; -const SHUTDOWN_TIMEOUT = 1000; -let isShuttingDown = false; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")); -const frontendPath = path.join(__dirname, "public"); - -let server; - -const shutdown = async () => { - if (isShuttingDown) { - return; - } - isShuttingDown = true; - logger.info({ message: "Attempting graceful shutdown" }); - setTimeout(async () => { - logger.error({ - message: "Could not shut down in time, forcing shutdown", - service: SERVICE_NAME, - method: "shutdown", - }); - await ServiceRegistry.get(RedisService.SERVICE_NAME).flushRedis(); - process.exit(1); - }, SHUTDOWN_TIMEOUT); - try { - server.close(); - await ServiceRegistry.get(JobQueue.SERVICE_NAME).shutdown(); - await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect(); - logger.info({ message: "Graceful shutdown complete" }); - process.exit(0); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "shutdown", - stack: error.stack, - }); - } -}; -// Need to wrap server setup in a function to handle async nature of JobQueue const startApp = async () => { - const app = express(); - // Create and Register Primary services - const translationService = new TranslationService(logger); - const stringService = new StringService(translationService); - ServiceRegistry.register(StringService.SERVICE_NAME, stringService); - + // FE path + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")); + const frontendPath = path.join(__dirname, "public"); // Create services const settingsService = new SettingsService(AppSettings); const appSettings = settingsService.loadSettings(); - // Create DB - const db = new MongoDB({ appSettings }); - await db.connect(); + // Initialize services + const services = await initializeServices(appSettings, settingsService); - // Set allowed origin - const allowedOrigin = appSettings.clientHost; + // Initialize controllers + const controllers = initializeControllers(services); - const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService); - const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); - const bufferService = new BufferService({ db, logger }); - const statusService = new StatusService({ db, logger, buffer: bufferService }); - const notificationUtils = new NotificationUtils({ - stringService, - emailService, - }); - - const notificationService = new NotificationService({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }); - - const redisService = new RedisService({ Redis: IORedis, logger }); - const errorService = new ErrorService(); - - // const jobQueueHelper = new JobQueueHelper({ - // redisService, - // Queue, - // Worker, - // logger, - // db, - // networkService, - // statusService, - // notificationService, - // }); - // const jobQueue = await JobQueue.create({ - // db, - // jobQueueHelper, - // logger, - // stringService, - // }); - - // const pulseQueueHelper = new PulseQueueHelper({ - // db, - // logger, - // networkService, - // statusService, - // notificationService, - // }); - // const pulseQueue = await PulseQueue.create({ - // appSettings, - // db, - // pulseQueueHelper, - // logger, - // }); - const superSimpleQueueHelper = new SuperSimpleQueueHelper({ - db, - logger, - networkService, - statusService, - notificationService, - }); - - const superSimpleQueue = await SuperSimpleQueue.create({ + const app = createApp({ + services, + controllers, appSettings, - db, - logger, - helper: superSimpleQueueHelper, + frontendPath, + openApiSpec, }); - // Business services - const userService = new UserService({ - db, - emailService, - settingsService, - logger, - stringService, - jwt, - errorService, - }); - const checkService = new CheckService({ - db, - settingsService, - stringService, - errorService, - }); - const diagnosticService = new DiagnosticService(); - const inviteService = new InviteService({ - db, - settingsService, - emailService, - stringService, - errorService, - }); - const maintenanceWindowService = new MaintenanceWindowService({ - db, - settingsService, - stringService, - errorService, - }); - const monitorService = new MonitorService({ - db, - settingsService, - jobQueue: superSimpleQueue, - stringService, - 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); - ServiceRegistry.register(NetworkService.SERVICE_NAME, networkService); - ServiceRegistry.register(BufferService.SERVICE_NAME, bufferService); - ServiceRegistry.register(StatusService.SERVICE_NAME, statusService); - ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService); - ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService); - ServiceRegistry.register(RedisService.SERVICE_NAME, redisService); - ServiceRegistry.register(UserService.SERVICE_NAME, userService); - ServiceRegistry.register(CheckService.SERVICE_NAME, checkService); - 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(); - const port = appSettings.port || 52345; - server = app.listen(port, () => { + const server = app.listen(port, () => { logger.info({ message: `Server started on port:${port}` }); }); - process.on("SIGUSR2", shutdown); - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - - //Create controllers - - 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), - userService: ServiceRegistry.get(UserService.SERVICE_NAME), - }); - - const monitorController = new MonitorController(commonDependencies, { - settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), - emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), - monitorService: ServiceRegistry.get(MonitorService.SERVICE_NAME), - }); - - const settingsController = new SettingsController(commonDependencies, { - settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - emailService: ServiceRegistry.get(EmailService.SERVICE_NAME), - }); - - const checkController = new CheckController(commonDependencies, { - settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - checkService: ServiceRegistry.get(CheckService.SERVICE_NAME), - }); - - const inviteController = new InviteController(commonDependencies, { - inviteService: ServiceRegistry.get(InviteService.SERVICE_NAME), - }); - - const maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { - settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME), - maintenanceWindowService: ServiceRegistry.get(MaintenanceWindowService.SERVICE_NAME), - }); - - const queueController = new QueueController(commonDependencies, { - jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME), - }); - - 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 - const authRoutes = new AuthRoutes(authController); - const monitorRoutes = new MonitorRoutes(monitorController); - const settingsRoutes = new SettingsRoutes(settingsController); - const checkRoutes = new CheckRoutes(checkController); - const inviteRoutes = new InviteRoutes(inviteController); - const maintenanceWindowRoutes = new MaintenanceWindowRoutes(maintenanceWindowController); - 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); - app.use( - cors({ - origin: allowedOrigin, - methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", - allowedHeaders: "*", - credentials: true, - }) - ); - app.use(express.json()); - app.use( - helmet({ - hsts: false, - contentSecurityPolicy: { - useDefaults: true, - directives: { - upgradeInsecureRequests: null, - }, - }, - }) - ); - app.use( - compression({ - level: 6, - threshold: 1024, - filter: (req, res) => { - if (req.headers["x-no-compression"]) { - return false; - } - return compression.filter(req, res); - }, - }) - ); - - app.use(languageMiddleware(stringService, translationService, settingsService)); - // Swagger UI - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); - - //routes - app.use("/api/v1/auth", authRoutes.getRouter()); - app.use("/api/v1/monitors", verifyJWT, monitorRoutes.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({ - status: "OK", - }); - }); - // FE routes - app.get("*", (req, res) => { - res.sendFile(path.join(frontendPath, "index.html")); - }); - app.use(handleErrors); + initShutdownListener(server, services); }; startApp().catch((error) => { diff --git a/server/server.js b/server/server.js new file mode 100644 index 000000000..13a01fae6 --- /dev/null +++ b/server/server.js @@ -0,0 +1 @@ +export const startServer = () => {}; diff --git a/server/service/business/checkService.js b/server/service/business/checkService.js index 3d8206644..262d8e863 100644 --- a/server/service/business/checkService.js +++ b/server/service/business/checkService.js @@ -10,6 +10,10 @@ class CheckService { this.errorService = errorService; } + get serviceName() { + return CheckService.SERVICE_NAME; + } + getChecksByMonitor = async ({ monitorId, query, teamId }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); diff --git a/server/service/business/diagnosticService.js b/server/service/business/diagnosticService.js index 5c37cba14..353604932 100644 --- a/server/service/business/diagnosticService.js +++ b/server/service/business/diagnosticService.js @@ -5,6 +5,7 @@ const SERVICE_NAME = "diagnosticService"; class DiagnosticService { static SERVICE_NAME = SERVICE_NAME; + constructor() { /** * Performance Observer for monitoring system performance metrics. @@ -18,6 +19,10 @@ class DiagnosticService { obs.observe({ entryTypes: ["measure"] }); } + get serviceName() { + return DiagnosticService.SERVICE_NAME; + } + getCPUUsage = async () => { const startUsage = process.cpuUsage(); const timingPeriod = 1000; // measured in ms diff --git a/server/service/business/inviteService.js b/server/service/business/inviteService.js index d23d725e4..161045971 100644 --- a/server/service/business/inviteService.js +++ b/server/service/business/inviteService.js @@ -11,6 +11,10 @@ class InviteService { this.errorService = errorService; } + get serviceName() { + return InviteService.SERVICE_NAME; + } + getInviteToken = async ({ invite, teamId }) => { invite.teamId = teamId; const inviteToken = await this.db.requestInviteToken(invite); diff --git a/server/service/business/maintenanceWindowService.js b/server/service/business/maintenanceWindowService.js index 0f58aca85..ec0fa5345 100644 --- a/server/service/business/maintenanceWindowService.js +++ b/server/service/business/maintenanceWindowService.js @@ -2,6 +2,7 @@ const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; + constructor({ db, settingsService, stringService, errorService }) { this.db = db; this.settingsService = settingsService; @@ -9,6 +10,10 @@ class MaintenanceWindowService { this.errorService = errorService; } + get serviceName() { + return MaintenanceWindowService.SERVICE_NAME; + } + createMaintenanceWindow = async ({ teamId, body }) => { const monitorIds = body.monitors; const monitors = await this.db.getMonitorsByIds(monitorIds); diff --git a/server/service/business/monitorService.js b/server/service/business/monitorService.js index 2a2b27aa3..d307e1837 100644 --- a/server/service/business/monitorService.js +++ b/server/service/business/monitorService.js @@ -2,7 +2,7 @@ import { createMonitorsBodyValidation } from "../../validation/joi.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { - SERVICE_NAME = SERVICE_NAME; + static SERVICE_NAME = SERVICE_NAME; constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService }) { this.db = db; @@ -15,6 +15,10 @@ class MonitorService { this.errorService = errorService; } + get serviceName() { + return MonitorService.SERVICE_NAME; + } + verifyTeamAccess = async ({ teamId, monitorId }) => { const monitor = await this.db.getMonitorById(monitorId); if (!monitor?.teamId?.equals(teamId)) { diff --git a/server/service/business/userService.js b/server/service/business/userService.js index 12dd4207f..e4296a9c7 100644 --- a/server/service/business/userService.js +++ b/server/service/business/userService.js @@ -2,6 +2,7 @@ const SERVICE_NAME = "userService"; class UserService { static SERVICE_NAME = SERVICE_NAME; + constructor({ db, emailService, settingsService, logger, stringService, jwt, errorService }) { this.db = db; this.emailService = emailService; @@ -12,6 +13,10 @@ class UserService { this.errorService = errorService; } + get serviceName() { + return UserService.SERVICE_NAME; + } + issueToken = (payload, appSettings) => { const tokenTTL = appSettings?.jwtTTL ?? "2h"; const tokenSecret = appSettings?.jwtSecret; diff --git a/server/service/data/redisService.js b/server/service/data/redisService.js index a89c21fab..3bcfe73b8 100644 --- a/server/service/data/redisService.js +++ b/server/service/data/redisService.js @@ -2,12 +2,17 @@ const SERVICE_NAME = "RedisService"; class RedisService { static SERVICE_NAME = SERVICE_NAME; + constructor({ Redis, logger }) { this.Redis = Redis; this.connections = new Set(); this.logger = logger; } + get serviceName() { + return RedisService.SERVICE_NAME; + } + getNewConnection(options = {}) { const connection = new this.Redis(process.env.REDIS_URL, { retryStrategy: (times) => { diff --git a/server/service/infrastructure/JobQueue/JobQueue.js b/server/service/infrastructure/JobQueue/JobQueue.js index 6e316c616..88f0a3cac 100644 --- a/server/service/infrastructure/JobQueue/JobQueue.js +++ b/server/service/infrastructure/JobQueue/JobQueue.js @@ -23,6 +23,10 @@ class JobQueue { this.workers = []; } + get serviceName() { + return JobQueue.SERVICE_NAME; + } + static async create({ db, jobQueueHelper, logger, stringService }) { const instance = new JobQueue({ db, jobQueueHelper, logger, stringService }); await instance.init(); diff --git a/server/service/infrastructure/JobQueue/JobQueueHelper.js b/server/service/infrastructure/JobQueue/JobQueueHelper.js index 4107966e8..c746edae1 100644 --- a/server/service/infrastructure/JobQueue/JobQueueHelper.js +++ b/server/service/infrastructure/JobQueue/JobQueueHelper.js @@ -1,6 +1,8 @@ const SERVICE_NAME = "JobQueueHelper"; class JobQueueHelper { + static SERVICE_NAME = SERVICE_NAME; + constructor({ redisService, Queue, Worker, logger, db, networkService, statusService, notificationService }) { this.db = db; this.redisService = redisService; @@ -12,6 +14,10 @@ class JobQueueHelper { this.notificationService = notificationService; } + get serviceName() { + return JobQueueHelper.SERVICE_NAME; + } + createQueue(queueName) { const connection = this.redisService.getNewConnection(); const q = new this.Queue(queueName, { diff --git a/server/service/infrastructure/PulseQueue/PulseQueue.js b/server/service/infrastructure/PulseQueue/PulseQueue.js index 85d14ab07..d822b371d 100644 --- a/server/service/infrastructure/PulseQueue/PulseQueue.js +++ b/server/service/infrastructure/PulseQueue/PulseQueue.js @@ -1,6 +1,6 @@ import { Pulse } from "@pulsecron/pulse"; -const SERVICE_NAME = "PulseQueue"; +const SERVICE_NAME = "JobQueue"; class PulseQueue { static SERVICE_NAME = SERVICE_NAME; @@ -11,6 +11,10 @@ class PulseQueue { this.logger = logger; } + get serviceName() { + return PulseQueue.SERVICE_NAME; + } + static async create({ appSettings, db, pulseQueueHelper, logger }) { const instance = new PulseQueue({ appSettings, db, pulseQueueHelper, logger }); await instance.init(); diff --git a/server/service/infrastructure/PulseQueue/PulseQueueHelper.js b/server/service/infrastructure/PulseQueue/PulseQueueHelper.js index c3b8992d8..a5ec34d01 100644 --- a/server/service/infrastructure/PulseQueue/PulseQueueHelper.js +++ b/server/service/infrastructure/PulseQueue/PulseQueueHelper.js @@ -1,6 +1,8 @@ const SERVICE_NAME = "PulseQueueHelper"; class PulseQueueHelper { + static SERVICE_NAME = SERVICE_NAME; + constructor({ db, logger, networkService, statusService, notificationService }) { this.db = db; this.logger = logger; @@ -9,6 +11,10 @@ class PulseQueueHelper { this.notificationService = notificationService; } + get serviceName() { + return PulseQueueHelper.SERVICE_NAME; + } + getMonitorJob = () => { return async (job) => { try { diff --git a/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js index f0b59640e..e9021bf97 100644 --- a/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -1,5 +1,5 @@ import Scheduler from "super-simple-scheduler"; -const SERVICE_NAME = "SuperSimpleQueue"; +const SERVICE_NAME = "JobQueue"; class SuperSimpleQueue { static SERVICE_NAME = SERVICE_NAME; @@ -11,6 +11,10 @@ class SuperSimpleQueue { this.helper = helper; } + get serviceName() { + return SuperSimpleQueue.SERVICE_NAME; + } + static async create({ appSettings, db, logger, helper }) { const instance = new SuperSimpleQueue({ appSettings, db, logger, helper }); await instance.init(); diff --git a/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index fd41a5179..639e5183b 100644 --- a/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -1,6 +1,7 @@ -const SERVICE_NAME = "SuperSimpleQueueHelper"; +const SERVICE_NAME = "JobQueueHelper"; class SuperSimpleQueueHelper { + static SERVICE_NAME = SERVICE_NAME; constructor({ db, logger, networkService, statusService, notificationService }) { this.db = db; this.logger = logger; @@ -9,6 +10,10 @@ class SuperSimpleQueueHelper { this.notificationService = notificationService; } + get serviceName() { + return SuperSimpleQueueHelper.SERVICE_NAME; + } + getMonitorJob = () => { return async (monitor) => { try { diff --git a/server/service/infrastructure/bufferService.js b/server/service/infrastructure/bufferService.js index 84f3efd30..439b251f6 100755 --- a/server/service/infrastructure/bufferService.js +++ b/server/service/infrastructure/bufferService.js @@ -10,6 +10,7 @@ const TYPE_MAP = { }; class BufferService { + static SERVICE_NAME = SERVICE_NAME; constructor({ db, logger }) { this.db = db; this.logger = logger; @@ -32,7 +33,10 @@ class BufferService { method: "constructor", }); } - static SERVICE_NAME = SERVICE_NAME; + + get serviceName() { + return BufferService.SERVICE_NAME; + } addToBuffer({ check, type }) { try { diff --git a/server/service/infrastructure/emailService.js b/server/service/infrastructure/emailService.js index b6b0eb4af..bc040d51a 100755 --- a/server/service/infrastructure/emailService.js +++ b/server/service/infrastructure/emailService.js @@ -33,6 +33,10 @@ class EmailService { this.init(); } + get serviceName() { + return EmailService.SERVICE_NAME; + } + init = async () => { /** * Loads an email template from the filesystem. @@ -42,7 +46,7 @@ class EmailService { */ this.loadTemplate = (templateName) => { try { - const templatePath = this.path.join(__dirname, `../templates/${templateName}.mjml`); + const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); } catch (error) { diff --git a/server/service/infrastructure/errorService.js b/server/service/infrastructure/errorService.js index 552816b73..680bb3765 100644 --- a/server/service/infrastructure/errorService.js +++ b/server/service/infrastructure/errorService.js @@ -55,9 +55,12 @@ class BadRequestError extends AppError { const SERVICE_NAME = "ErrorService"; class ErrorService { static SERVICE_NAME = SERVICE_NAME; - constructor() {} + get serviceName() { + return ErrorService.SERVICE_NAME; + } + createError = (message, status = 500, service = null, method = null, details = null) => { return new AppError(message, status, service, method, details); }; diff --git a/server/service/infrastructure/networkService.js b/server/service/infrastructure/networkService.js index 70d7cc932..6a2e3c8a7 100755 --- a/server/service/infrastructure/networkService.js +++ b/server/service/infrastructure/networkService.js @@ -36,6 +36,10 @@ class NetworkService { this.settingsService = settingsService; } + get serviceName() { + return NetworkService.SERVICE_NAME; + } + /** * Times the execution of an asynchronous operation. * diff --git a/server/service/infrastructure/notificationService.js b/server/service/infrastructure/notificationService.js index f253cf760..6af4fd20a 100644 --- a/server/service/infrastructure/notificationService.js +++ b/server/service/infrastructure/notificationService.js @@ -12,6 +12,10 @@ class NotificationService { this.notificationUtils = notificationUtils; } + get serviceName() { + return NotificationService.SERVICE_NAME; + } + sendNotification = async ({ notification, subject, content, html }) => { const { type, address } = notification; diff --git a/server/service/infrastructure/notificationUtils.js b/server/service/infrastructure/notificationUtils.js index d070a9995..22c7f4df7 100644 --- a/server/service/infrastructure/notificationUtils.js +++ b/server/service/infrastructure/notificationUtils.js @@ -1,9 +1,17 @@ +const SERVICE_NAME = "NotificationUtils"; + class NotificationUtils { + static SERVICE_NAME = SERVICE_NAME; + constructor({ stringService, emailService }) { this.stringService = stringService; this.emailService = emailService; } + get serviceName() { + return NotificationUtils.SERVICE_NAME; + } + buildTestEmail = async () => { const context = { testName: "Monitoring System" }; const html = await this.emailService.buildEmail("testEmailTemplate", context); diff --git a/server/service/infrastructure/statusService.js b/server/service/infrastructure/statusService.js index 1c91a407c..3e2330380 100755 --- a/server/service/infrastructure/statusService.js +++ b/server/service/infrastructure/statusService.js @@ -4,6 +4,7 @@ const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; + /** * Creates an instance of StatusService. * @@ -14,7 +15,10 @@ class StatusService { this.db = db; this.logger = logger; this.buffer = buffer; - this.SERVICE_NAME = SERVICE_NAME; + } + + get serviceName() { + return StatusService.SERVICE_NAME; } async updateRunningStats({ monitor, networkResponse }) { diff --git a/server/service/system/serviceRegistry.js b/server/service/system/serviceRegistry.js index 5f8b3bfe6..6aa3f94ef 100755 --- a/server/service/system/serviceRegistry.js +++ b/server/service/system/serviceRegistry.js @@ -6,6 +6,10 @@ class ServiceRegistry { this.services = {}; } + get serviceName() { + return ServiceRegistry.SERVICE_NAME; + } + register(name, service) { logger.info({ message: `Registering service ${name}`, diff --git a/server/service/system/settingsService.js b/server/service/system/settingsService.js index d2c1a17ae..4f5e18abb 100755 --- a/server/service/system/settingsService.js +++ b/server/service/system/settingsService.js @@ -1,4 +1,5 @@ const SERVICE_NAME = "SettingsService"; + const envConfig = { nodeEnv: process.env.NODE_ENV, logLevel: process.env.LOG_LEVEL, @@ -23,7 +24,8 @@ const envConfig = { * This service is responsible for loading and managing the application settings. */ class SettingsService { - static SERVICE_NAME = SERVICE_NAME; + static SERVICE_NAME = "SettingsService"; + /** * Constructs a new SettingsService * @constructor @@ -32,6 +34,11 @@ class SettingsService { this.AppSettings = AppSettings; this.settings = { ...envConfig }; } + + get serviceName() { + return SettingsService.SERVICE_NAME; + } + /** * Load settings from env settings * @returns {Object>} The settings. diff --git a/server/service/system/stringService.js b/server/service/system/stringService.js index b88e73917..5df3410f9 100755 --- a/server/service/system/stringService.js +++ b/server/service/system/stringService.js @@ -1,5 +1,7 @@ +const SERVICE_NAME = "StringService"; + class StringService { - static SERVICE_NAME = "StringService"; + static SERVICE_NAME = SERVICE_NAME; constructor(translationService) { if (StringService.instance) { @@ -11,6 +13,10 @@ class StringService { StringService.instance = this; } + get serviceName() { + return StringService.SERVICE_NAME; + } + setLanguage(language) { this._language = language; } diff --git a/server/service/system/translationService.js b/server/service/system/translationService.js index 38346f35d..20b3431e0 100755 --- a/server/service/system/translationService.js +++ b/server/service/system/translationService.js @@ -3,7 +3,6 @@ import path from "path"; class TranslationService { static SERVICE_NAME = "TranslationService"; - constructor(logger) { this.logger = logger; this.translations = {}; @@ -11,6 +10,10 @@ class TranslationService { this.localesDir = path.join(process.cwd(), "locales"); } + get serviceName() { + return TranslationService.SERVICE_NAME; + } + setLanguage(language) { this._language = language; } diff --git a/server/service/userService.js b/server/service/userService.js deleted file mode 100644 index 3e1bf285f..000000000 --- a/server/service/userService.js +++ /dev/null @@ -1,208 +0,0 @@ -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 }) { - this.db = db; - this.emailService = emailService; - this.settingsService = settingsService; - this.logger = logger; - this.stringService = stringService; - this.jwt = jwt; - } - - issueToken = (payload, appSettings) => { - const tokenTTL = appSettings?.jwtTTL ?? "2h"; - const tokenSecret = appSettings?.jwtSecret; - const payloadData = payload; - return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); - }; - - registerUser = async (user, file) => { - // Create a new user - // If superAdmin exists, a token should be attached to all further register requests - const superAdminExists = await this.db.checkSuperadmin(); - if (superAdminExists) { - const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken); - user.role = invitedUser.role; - user.teamId = invitedUser.teamId; - } else { - // This is the first account, create JWT secret to use if one is not supplied by env - const jwtSecret = crypto.randomBytes(64).toString("hex"); - await this.db.updateAppSettings({ jwtSecret }); - } - - const newUser = await this.db.insertUser({ ...user }, file); - - this.logger.debug({ - message: "New user created", - service: SERVICE_NAME, - method: "registerUser", - details: newUser._id, - }); - - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; - - const appSettings = await this.settingsService.getSettings(); - - const token = this.issueToken(userForToken, appSettings); - - try { - const html = await this.emailService.buildEmail("welcomeEmailTemplate", { - name: newUser.firstName, - }); - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); - }); - } catch (error) { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); - } - - return { user: newUser, token }; - }; - - loginUser = async (email, password) => { - // Check if user exists - const user = await this.db.getUserByEmail(email); - // Compare password - const match = await user.comparePassword(password); - if (match !== true) { - throw createAuthError(this.stringService.authIncorrectPassword); - } - - // Remove password from user object. Should this be abstracted to DB layer? - const userWithoutPassword = { ...user._doc }; - delete userWithoutPassword.password; - delete userWithoutPassword.avatarImage; - - // Happy path, return token - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(userWithoutPassword, appSettings); - // reset avatar image - userWithoutPassword.avatarImage = user.avatarImage; - return { user: userWithoutPassword, token }; - }; - - editUser = async (updates, file, currentUser) => { - // Change Password check - if (updates?.password && updates?.newPassword) { - // Get user's email - // Add user email to body for DB operation - updates.email = currentUser.email; - // Get user - const user = await this.db.getUserByEmail(currentUser.email); - // Compare passwords - const match = await user.comparePassword(updates?.password); - // If not a match, throw a 403 - // 403 instead of 401 to avoid triggering axios interceptor - if (!match) { - throw createError(this.stringService.authIncorrectPassword, 403); - } - // If a match, update the password - updates.password = updates.newPassword; - } - - const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file }); - return updatedUser; - }; - - checkSuperadminExists = async () => { - const superAdminExists = await this.db.checkSuperadmin(); - return superAdminExists; - }; - - requestRecovery = async (email) => { - const user = await this.db.getUserByEmail(email); - const recoveryToken = await this.db.requestRecoveryToken(email); - const name = user.firstName; - const { clientHost } = this.settingsService.getSettings(); - const url = `${clientHost}/set-new-password/${recoveryToken.token}`; - - const html = await this.emailService.buildEmail("passwordResetTemplate", { - name, - email, - url, - }); - const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); - return msgId; - }; - - validateRecovery = async (recoveryToken) => { - await this.db.validateRecoveryToken(recoveryToken); - }; - - resetPassword = async (password, recoveryToken) => { - const user = await this.db.resetPassword(password, recoveryToken); - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(user._doc, appSettings); - return { user, token }; - }; - - deleteUser = async (user) => { - const email = user?.email; - if (!email) { - throw new Error("No email in request"); - } - - const teamId = user?.teamId; - const userId = user?._id; - - if (!teamId) { - throw new Error("No team ID in request"); - } - - if (!userId) { - throw new Error("No user ID in request"); - } - - const roles = user?.role; - if (roles.includes("demo")) { - throw new Error("Demo user cannot be deleted"); - } - - // 1. Find all the monitors associated with the team ID if superadmin - const result = await this.db.getMonitorsByTeamId({ - teamId: teamId, - }); - - if (roles.includes("superadmin")) { - // 2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && - (await Promise.all( - result.monitors.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor); - }) - )); - } - // 6. Delete the user by id - await this.db.deleteUser(userId); - }; - - getAllUsers = async () => { - const users = await this.db.getAllUsers(); - return users; - }; - - getUserById = async (roles, userId) => { - const user = await this.db.getUserById(roles, userId); - return user; - }; - - editUserById = async (userId, user) => { - await this.db.editUserById(userId, user); - }; -} -export default UserService; diff --git a/server/shutdown.js b/server/shutdown.js new file mode 100644 index 000000000..b9b2b53c5 --- /dev/null +++ b/server/shutdown.js @@ -0,0 +1,34 @@ +import logger from "./utils/logger.js"; + +export const initShutdownListener = (server, services) => { + const SERVICE_NAME = "Server"; + + let isShuttingDown = false; + + const shutdown = async () => { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + logger.info({ message: "Attempting graceful shutdown" }); + + try { + server.close(); + await services.jobQueue.shutdown(); + await services.db.disconnect(); + logger.info({ message: "Graceful shutdown complete" }); + process.exit(0); + } catch (error) { + logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "shutdown", + stack: error.stack, + }); + process.exit(1); + } + }; + process.on("SIGUSR2", shutdown); + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +}; diff --git a/server/utils/logger.js b/server/utils/logger.js index 8cb57f9a7..a94ad6be2 100755 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -61,6 +61,11 @@ class Logger { ], }); } + + get serviceName() { + return Logger.SERVICE_NAME; + } + /** * Logs an informational message. * @param {Object} config - The configuration object.