Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad
+
+
+
+Este repositorio contiene tanto el frontend como el backend de Checkmate, una herramienta de monitoreo de código abierto y autoalojada para rastrear hardware de servidores, tiempo de actividad, tiempos de respuesta e incidentes en tiempo real con visualizaciones atractivas. Checkmate revisa regularmente si un servidor o sitio web es accesible y funciona de manera óptima, proporcionando alertas e informes en tiempo real sobre la disponibilidad, el tiempo de inactividad y el tiempo de respuesta de los servicios monitoreados.
+
+Checkmate también tiene un agente llamado [Capture](https://github.com/bluewave-labs/capture), para recuperar datos de servidores remotos. Aunque Capture no es obligatorio para ejecutar Checkmate, proporciona información adicional sobre el estado de la CPU, RAM, disco y temperatura de tus servidores.
+
+Checkmate ha sido probado con más de 1000 monitores activos sin problemas ni cuellos de botella de rendimiento.
+
+**Si deseas patrocinar una función, [visita este enlace](https://checkmate.so/sponsored-features).**
+
+## 📚 Tabla de contenidos
+
+- [📦 Demo](#demo)
+- [🔗 Guía del usuario](#guía-del-usuario)
+- [🛠️ Instalación](#instalación)
+- [🏁 Traducciones](#traducciones)
+- [🚀 Rendimiento](#rendimiento)
+- [💚 Preguntas e ideas](#preguntas-e-ideas)
+- [🧩 Características](#características)
+- [🏗️ Capturas de pantalla](#capturas-de-pantalla)
+- [🏗️ Tecnologías](#tecnologías)
+- [🔗 Enlaces útiles](#enlaces-útiles)
+- [🤝 Contribuciones](#contribuciones)
+- [💰 Patrocinadores](#patrocinadores)
+
+...
+
+**[Texto truncado para mantener la longitud del mensaje manejable]**
+
+## Demo
+
+Puedes ver la última versión de [Checkmate](https://checkmate-demo.bluewavelabs.ca/) en acción. El usuario es uptimedemo@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando, así que si no funciona para ti, por favor contáctanos en el canal de Discusiones).
+
+## Guía del usuario
+
+Las instrucciones de uso se pueden encontrar [aquí](https://docs.checkmate.so/checkmate-2.1). Todavía está en desarrollo y parte de la información puede estar desactualizada ya que continuamente añadimos funciones cada semana. ¡Ten por seguro que estamos haciendo lo mejor posible! :)
+
+## Instalación
+
+Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
+
+Alternativamente, también puedes usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) o [Pikapods](https://www.pikapods.com/) para desplegar rápidamente una instancia de Checkmate. Si deseas monitorear tu infraestructura de servidores, necesitarás el agente [Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también contiene las instrucciones de instalación.
+
+## Traducciones
+
+Si deseas usar Checkmate en tu idioma, por favor [ve a esta página](https://poeditor.com/join/project/lRUoGZFCsJ) y regístrate para el idioma al que te gustaría traducir Checkmate.
+
+## Rendimiento
+
+Gracias a extensas optimizaciones, Checkmate opera con un uso de memoria excepcionalmente bajo, requiriendo recursos mínimos de memoria y CPU. Aquí está el uso de memoria de una instancia de Node.js ejecutándose en un servidor que monitorea 323 servidores cada minuto:
+
+
+
+También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servidor (398Mb y 15Mb) para la misma cantidad de servidores:
+
+
+
+## Preguntas e Ideas
+
+Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones:
+
+- [Canal de Discord](https://discord.gg/NAb6H3UTjK)
+- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions)
+- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
+
+¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
+
+## Características
+
+- Completamente de código abierto, desplegable en tus propios servidores o dispositivos personales (por ejemplo, Raspberry Pi 4 o 5)
+- Monitoreo de sitios web
+- Monitoreo de velocidad de carga
+- Monitoreo de infraestructura (memoria, uso de disco, rendimiento de CPU, etc) - requiere el agente [Capture](https://github.com/bluewave-labs/capture)
+- Monitoreo de Docker
+- Monitoreo con ping
+- Monitoreo de certificados SSL
+- Monitoreo de puertos
+- Incidentes en una sola vista
+- Páginas de estado
+- Notificaciones por correo, Webhooks, Discord, Telegram, Slack
+- Mantenimiento programado
+- Monitoreo de consultas JSON
+- Soporte para múltiples idiomas
+
+**Hoja de ruta a corto plazo:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
+
+- Mejores notificaciones
+- Monitoreo de red
+- ...y algunas funciones más
+
+## Capturas de pantalla
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Tecnologías
+
+- [ReactJs](https://react.dev/)
+- [MUI (framework de React)](https://mui.com/)
+- [Node.js](https://nodejs.org/en)
+- [MongoDB](https://mongodb.com)
+- [Recharts](https://recharts.org)
+- ¡Y muchos otros componentes de código abierto!
+
+## Enlaces útiles
+
+- Si deseas apoyarnos, por favor considera darle una ⭐ y haz clic en "watch".
+- ¿Tienes una pregunta o sugerencia para la hoja de ruta? Revisa nuestro [canal de Discord](https://discord.gg/NAb6H3UTjK) o el foro de [Discusiones](https://github.com/bluewave-labs/checkmate/discussions).
+- ¿Quieres saber cuándo hay una nueva versión? Usa [Newreleases](https://newreleases.io/), un servicio gratuito para seguir lanzamientos.
+- Mira un video de instalación y uso de Checkmate [aquí](https://www.youtube.com/watch?v=GfFOc0xHIwY)
+
+## Contribuciones
+
+Somos [Alex](http://github.com/ajhollid) (líder de equipo), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) y [Mert](https://github.com/mertssmnoglu), ayudando a personas y empresas a monitorear su infraestructura y servidores.
+
+Nos enorgullecemos de construir conexiones fuertes con contribuyentes de todos los niveles. A pesar de ser un proyecto joven, Checkmate ya ha ganado más de 7000 estrellas y ha atraído a más de 90 contribuyentes de todo el mundo.
+
+Nuestro repositorio ha sido marcado con estrella por empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC**, ¡así que no te detengas — participa, contribuye y aprende con nosotros!
+
+Cómo contribuir:
+
+0. Dale una estrella al repositorio :)
+1. Revisa la [guía para contribuidores](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se anima a los nuevos a revisar las etiquetas `good-first-issue`.
+2. Consulta la [estructura del proyecto](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) y la [visión general](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
+3. Lee una estructura detallada de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si deseas profundizar en la arquitectura.
+4. Abre un issue si crees que has encontrado un error.
+5. Revisa los issues con la etiqueta `good-first-issue` si eres nuevo.
+6. Haz un pull request para añadir nuevas funciones, mejoras o correcciones.
+
+
+
+
+
+[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
+
+## Patrocinadores
+
+Gracias a [Gitbook](https://gitbook.io/) por darnos una cuenta gratuita para su plataforma de documentación, y a [Poeditor](https://poeditor.com/) por proporcionarnos una cuenta gratuita para servicios de traducción. Si deseas patrocinar Checkmate, por favor envía un correo a hello@bluewavelabs.ca
+
+Si deseas patrocinar una función, [visita esta página](https://checkmate.so/sponsored-features).
+
+También puedes revisar otros proyectos de BlueWave orientados a desarrolladores y contribuidores:
+
+- [VerifyWise](https://github.com/bluewave-labs/verifywise), la primera plataforma de gobernanza de IA de código abierto.
+- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), una aplicación de intercambio de archivos seguro, también conocida como dataroom.
+- [Guidefox](https://github.com/bluewave-labs/guidefox), una app que ayuda a nuevos usuarios a aprender a usar tu producto mediante pistas, tours, popups y banners.
diff --git a/client/src/Hooks/useMonitorUtils.js b/client/src/Hooks/useMonitorUtils.js
index 417c6eadd..b57c11a12 100644
--- a/client/src/Hooks/useMonitorUtils.js
+++ b/client/src/Hooks/useMonitorUtils.js
@@ -6,18 +6,18 @@ const useMonitorUtils = () => {
let uptimePercentage = "";
let percentageColor = "";
- if (monitor.uptimePercentage !== undefined) {
+ if (monitor?.uptimePercentage !== undefined) {
uptimePercentage =
- monitor.uptimePercentage === 0
+ monitor?.uptimePercentage === 0
? "0"
- : (monitor.uptimePercentage * 100).toFixed(2);
+ : (monitor?.uptimePercentage * 100).toFixed(2);
percentageColor =
- monitor.uptimePercentage < 0.25
+ monitor?.uptimePercentage < 0.25
? theme.palette.error.main
- : monitor.uptimePercentage < 0.5
+ : monitor?.uptimePercentage < 0.5
? theme.palette.warning.main
- : monitor.uptimePercentage < 0.75
+ : monitor?.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
@@ -32,7 +32,7 @@ const useMonitorUtils = () => {
const determineState = useCallback((monitor) => {
if (typeof monitor === "undefined") return "pending";
- if (monitor.isActive === false) return "paused";
+ if (monitor?.isActive === false) return "paused";
if (monitor?.status === undefined) return "pending";
return monitor?.status == true ? "up" : "down";
}, []);
diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx
index 2dacc26ab..061db6ac0 100644
--- a/client/src/Pages/Uptime/Configure/index.jsx
+++ b/client/src/Pages/Uptime/Configure/index.jsx
@@ -163,18 +163,18 @@ const Configure = () => {
e.preventDefault();
const toSubmit = {
- _id: form._id,
- url: form.url,
- name: form.name,
- type: form.type,
- matchMethod: form.matchMethod,
- expectedValue: form.expectedValue,
- jsonPath: form.jsonPath,
- interval: form.interval,
- teamId: form.teamId,
- userId: form.userId,
- port: form.port,
- ignoreTlsErrors: form.ignoreTlsErrors,
+ _id: form?._id,
+ url: form?.url,
+ name: form?.name,
+ type: form?.type,
+ matchMethod: form?.matchMethod,
+ expectedValue: form?.expectedValue,
+ jsonPath: form?.jsonPath,
+ interval: form?.interval,
+ teamId: form?.teamId,
+ userId: form?.userId,
+ port: form?.port,
+ ignoreTlsErrors: form?.ignoreTlsErrors,
};
if (!useAdvancedMatching) {
@@ -197,7 +197,7 @@ const Configure = () => {
return;
}
- toSubmit.notifications = form.notifications;
+ toSubmit.notifications = form?.notifications;
await updateMonitor({ monitor: toSubmit, redirect: "/uptime" });
};
@@ -235,7 +235,7 @@ const Configure = () => {
component="h1"
variant="monitorName"
>
- {form.name}
+ {form?.name}
{
component="h2"
variant="monitorUrl"
>
- {form.url?.replace(/^https?:\/\//, "") || "..."}
+ {form?.url?.replace(/^https?:\/\//, "") || "..."}
{
type="number"
label={t("portToMonitor")}
placeholder="5173"
- value={form.port || ""}
+ value={form?.port || ""}
onChange={onChange}
error={errors["port"] ? true : false}
helperText={errors["port"]}
- hidden={form.type !== "port"}
+ hidden={form?.type !== "port"}
/>
{
@@ -398,7 +398,7 @@ const Configure = () => {
control={
@@ -430,12 +430,12 @@ const Configure = () => {
isChecked={useAdvancedMatching}
onChange={onChange}
/>
- {form.type === "http" && useAdvancedMatching && (
+ {form?.type === "http" && useAdvancedMatching && (
<>
@@ -445,8 +445,8 @@ const Configure = () => {
name="expectedValue"
label={t("expectedValue")}
isOptional={true}
- placeholder={expectedValuePlaceholders[form.matchMethod || "equal"]}
- value={form.expectedValue}
+ placeholder={expectedValuePlaceholders[form?.matchMethod || "equal"]}
+ value={form?.expectedValue}
onChange={onChange}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
@@ -466,7 +466,7 @@ const Configure = () => {
label="JSON Path"
isOptional={true}
placeholder="data.status"
- value={form.jsonPath}
+ value={form?.jsonPath}
onChange={onChange}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 3abd726da..59e2a0a21 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -111,30 +111,6 @@ class NetworkService {
return this.axiosInstance.post(`/monitors`, monitor);
}
- /**
- *
- * ************************************
- * Check the endpoint resolution
- * ************************************
- *
- * @async
- * @param {Object} config - The configuration object.
- * @param {Object} config.monitorURL - The monitor url to be sent in the request body.
- * @returns {Promise} The response from the axios POST request.
- */
- async checkEndpointResolution(config) {
- const { monitorURL } = config;
- const params = new URLSearchParams();
-
- if (monitorURL) params.append("monitorURL", monitorURL);
-
- return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
- headers: {
- "Content-Type": "application/json",
- },
- });
- }
-
/**
* ************************************
* Get all uptime monitors for a Team
diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile
index d9d99a09d..a3542c168 100755
--- a/docker/dev/server.Dockerfile
+++ b/docker/dev/server.Dockerfile
@@ -10,4 +10,4 @@ COPY ./server/ ./
EXPOSE 52345
-CMD ["node", "index.js"]
\ No newline at end of file
+CMD ["node", "src/index.js"]
\ No newline at end of file
diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile
index 981d1cf8f..c6d31a893 100644
--- a/docker/dist-arm/server.Dockerfile
+++ b/docker/dist-arm/server.Dockerfile
@@ -22,4 +22,4 @@ RUN chmod +x ./scripts/inject-vars.sh
EXPOSE 52345
-CMD ./scripts/inject-vars.sh && node ./index.js
+CMD ./scripts/inject-vars.sh && node ./src/index.js
diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile
index f28c834ea..4063e4d7a 100644
--- a/docker/dist-mono/server.Dockerfile
+++ b/docker/dist-mono/server.Dockerfile
@@ -22,4 +22,4 @@ RUN chmod +x ./scripts/inject-vars.sh
EXPOSE 52345
-CMD ./scripts/inject-vars.sh && node ./index.js
+CMD ./scripts/inject-vars.sh && node ./src/index.js
diff --git a/docker/dist/build_images.sh b/docker/dist/build_images.sh
index ccfa9cc85..953682175 100755
--- a/docker/dist/build_images.sh
+++ b/docker/dist/build_images.sh
@@ -12,10 +12,10 @@ declare -A services=(
)
for service in "${!services[@]}"; do
- docker buildx build \
- --platform linux/amd64,linux/arm64 \
+ docker build \
-f "${services[$service]}" \
-t "$service" \
+ .
if [ $? -ne 0 ]; then
echo "Error building $service image. Exiting..."
diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile
index d9d99a09d..a3542c168 100755
--- a/docker/dist/server.Dockerfile
+++ b/docker/dist/server.Dockerfile
@@ -10,4 +10,4 @@ COPY ./server/ ./
EXPOSE 52345
-CMD ["node", "index.js"]
\ No newline at end of file
+CMD ["node", "src/index.js"]
\ No newline at end of file
diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile
index c80f468f8..94018c07d 100755
--- a/docker/prod/server.Dockerfile
+++ b/docker/prod/server.Dockerfile
@@ -12,4 +12,4 @@ COPY ./server ./
EXPOSE 52345
-CMD ["node", "index.js"]
\ No newline at end of file
+CMD ["node", "src/index.js"]
\ No newline at end of file
diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile
index c80f468f8..94018c07d 100755
--- a/docker/staging/server.Dockerfile
+++ b/docker/staging/server.Dockerfile
@@ -12,4 +12,4 @@ COPY ./server ./
EXPOSE 52345
-CMD ["node", "index.js"]
\ No newline at end of file
+CMD ["node", "src/index.js"]
\ No newline at end of file
diff --git a/server/controllers/logController.js b/server/controllers/logController.js
deleted file mode 100644
index f4887ce9a..000000000
--- a/server/controllers/logController.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { asyncHandler } from "../utils/errorUtils.js";
-
-const SERVICE_NAME = "LogController";
-
-class LogController {
- constructor(logger) {
- this.logger = logger;
- }
-
- getLogs = asyncHandler(
- async (req, res, next) => {
- const logs = await this.logger.getLogs();
- res.success({
- msg: "Logs fetched successfully",
- data: logs,
- });
- },
- SERVICE_NAME,
- "getLogs"
- );
-}
-export default LogController;
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
deleted file mode 100755
index 9943a765a..000000000
--- a/server/controllers/monitorController.js
+++ /dev/null
@@ -1,708 +0,0 @@
-import {
- getMonitorByIdParamValidation,
- getMonitorByIdQueryValidation,
- getMonitorsByTeamIdParamValidation,
- getMonitorsByTeamIdQueryValidation,
- createMonitorBodyValidation,
- createMonitorsBodyValidation,
- getMonitorURLByQueryValidation,
- editMonitorBodyValidation,
- pauseMonitorParamValidation,
- getMonitorStatsByIdParamValidation,
- getMonitorStatsByIdQueryValidation,
- getCertificateParamValidation,
- getHardwareDetailsByIdParamValidation,
- getHardwareDetailsByIdQueryValidation,
-} from "../validation/joi.js";
-import sslChecker from "ssl-checker";
-import logger from "../utils/logger.js";
-import axios from "axios";
-import seedDb from "../db/mongo/utils/seedDb.js";
-import pkg from "papaparse";
-import { asyncHandler, createServerError } from "../utils/errorUtils.js";
-import { fetchMonitorCertificate } from "./controllerUtils.js";
-
-const SERVICE_NAME = "monitorController";
-class MonitorController {
- constructor(db, settingsService, jobQueue, stringService, emailService) {
- this.db = db;
- this.settingsService = settingsService;
- this.jobQueue = jobQueue;
- this.stringService = stringService;
- this.emailService = emailService;
- }
-
- 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;
- }
- }
-
- /**
- * Returns all monitors
- * @async
- * @param {Express.Request} req
- * @param {Express.Response} res
- * @param {function} next
- * @returns {Promise}
- * @throws {Error}
- */
- getAllMonitors = asyncHandler(
- async (req, res, next) => {
- const monitors = await this.db.getAllMonitors();
- return res.success({
- msg: this.stringService.monitorGetAll,
- data: monitors,
- });
- },
- SERVICE_NAME,
- "getAllMonitors"
- );
-
- getUptimeDetailsById = asyncHandler(
- async (req, res, next) => {
- const { monitorId } = req.params;
- const { dateRange, normalize } = req.query;
-
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- await this.verifyTeamAccess(teamId, monitorId);
-
- const data = await this.db.getUptimeDetailsById({
- monitorId,
- dateRange,
- normalize,
- });
- return res.success({
- msg: this.stringService.monitorGetByIdSuccess,
- data,
- });
- },
- SERVICE_NAME,
- "getUptimeDetailsById"
- );
-
- /**
- * Returns monitor stats for monitor with matching ID
- * @async
- * @param {Express.Request} req
- * @param {Express.Response} res
- * @param {function} next
- * @returns {Promise}
- * @throws {Error}
- */
- getMonitorStatsById = asyncHandler(
- async (req, res, next) => {
- await getMonitorStatsByIdParamValidation.validateAsync(req.params);
- await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
-
- let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
- const { monitorId } = req.params;
-
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- await this.verifyTeamAccess(teamId, monitorId);
-
- const monitorStats = await this.db.getMonitorStatsById({
- monitorId,
- limit,
- sortOrder,
- dateRange,
- numToDisplay,
- normalize,
- });
- return res.success({
- msg: this.stringService.monitorStatsById,
- data: monitorStats,
- });
- },
- SERVICE_NAME,
- "getMonitorStatsById"
- );
-
- /**
- * Get hardware details for a specific monitor by ID
- * @async
- * @param {Express.Request} req - Express request object containing monitorId in params
- * @param {Express.Response} res - Express response object
- * @param {Express.NextFunction} next - Express next middleware function
- * @returns {Promise}
- * @throws {Error} - Throws error if monitor not found or other database errors
- */
- getHardwareDetailsById = asyncHandler(
- async (req, res, next) => {
- await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
- await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
-
- const monitorId = req?.params?.monitorId;
- const dateRange = req?.query?.dateRange;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- await this.verifyTeamAccess(teamId, monitorId);
- const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
- return res.success({
- msg: this.stringService.monitorGetByIdSuccess,
- data: monitor,
- });
- },
- SERVICE_NAME,
- "getHardwareDetailsById"
- );
-
- getMonitorCertificate = asyncHandler(
- async (req, res, next) => {
- await getCertificateParamValidation.validateAsync(req.params);
-
- const { monitorId } = req.params;
- const monitor = await this.db.getMonitorById(monitorId);
- const certificate = await fetchMonitorCertificate(sslChecker, monitor);
-
- return res.success({
- msg: this.stringService.monitorCertificate,
- data: {
- certificateDate: new Date(certificate.validTo),
- },
- });
- },
- SERVICE_NAME,
- "getMonitorCertificate"
- );
-
- /**
- * Retrieves a monitor by its ID.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.params - The parameters of the request.
- * @property {string} req.params.monitorId - The ID of the monitor to be retrieved.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message, and the retrieved monitor data.
- * @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
- */
- getMonitorById = asyncHandler(
- async (req, res, next) => {
- await getMonitorByIdParamValidation.validateAsync(req.params);
- await getMonitorByIdQueryValidation.validateAsync(req.query);
-
- const monitor = await this.db.getMonitorById(req.params.monitorId);
-
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- if (!monitor.teamId.equals(teamId)) {
- const error = new Error("Unauthorized");
- error.status = 403;
- throw error;
- }
-
- return res.success({
- msg: this.stringService.monitorGetByIdSuccess,
- data: monitor,
- });
- },
- SERVICE_NAME,
- "getMonitorById"
- );
-
- /**
- * Creates a new monitor and adds it to the job queue.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.body - The body of the request.
- * @property {Array} req.body.notifications - The notifications associated with the monitor.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
- * @throws {Error} If there is an error during the process, especially if there is a validation error (422).
- */
- createMonitor = asyncHandler(
- async (req, res, next) => {
- await createMonitorBodyValidation.validateAsync(req.body);
-
- const { _id, teamId } = req.user;
- const monitor = await this.db.createMonitor({
- body: req.body,
- teamId,
- userId: _id,
- });
-
- // Add monitor to job queue
- this.jobQueue.addJob(monitor._id, monitor);
- return res.success({
- msg: this.stringService.monitorCreate,
- data: monitor,
- });
- },
- SERVICE_NAME,
- "createMonitor"
- );
-
- /**
- * Creates bulk monitors and adds them to the job queue after parsing CSV.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.file - The uploaded CSV file.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status and message.
- * @throws {Error} If there is an error during the process, especially if there is a validation error (422).
- */
- createBulkMonitors = asyncHandler(
- async (req, res, next) => {
- const { parse } = pkg;
-
- // validate the file
- if (!req.file) {
- throw new Error("No file uploaded");
- }
-
- // Check if the file is a CSV
- if (!req.file.mimetype.includes("csv")) {
- throw new Error("File is not a CSV");
- }
-
- // Validate if the file is empty
- if (req.file.size === 0) {
- throw new Error("File is empty");
- }
-
- const { _id, teamId } = req.user;
-
- if (!_id || !teamId) {
- throw new Error("Missing userId or teamId");
- }
-
- // Get file buffer from memory and convert to string
- const fileData = req.file.buffer.toString("utf-8");
-
- // Parse the CSV data
- parse(fileData, {
- header: true,
- skipEmptyLines: true,
- transform: (value, header) => {
- if (value === "") return undefined; // Empty fields become undefined
-
- // Handle 'port' and 'interval' fields, check if they're valid numbers
- if (["port", "interval"].includes(header)) {
- const num = parseInt(value, 10);
- if (isNaN(num)) {
- throw new Error(`${header} should be a valid number, got: ${value}`);
- }
- return num;
- }
-
- return value;
- },
- complete: async ({ data, errors }) => {
- if (errors.length > 0) {
- throw createServerError("Error parsing CSV");
- }
-
- if (!data || data.length === 0) {
- throw createServerError("CSV file contains no data rows");
- }
-
- const enrichedData = data.map((monitor) => ({
- userId: _id,
- teamId,
- ...monitor,
- description: monitor.description || monitor.name || monitor.url,
- name: monitor.name || monitor.url,
- type: monitor.type || "http",
- }));
-
- await createMonitorsBodyValidation.validateAsync(enrichedData);
-
- const monitors = await this.db.createBulkMonitors(enrichedData);
-
- await Promise.all(
- monitors.map(async (monitor, index) => {
- this.jobQueue.addJob(monitor._id, monitor);
- })
- );
-
- return res.success({
- msg: this.stringService.bulkMonitorsCreate,
- data: monitors,
- });
- },
- });
- },
- SERVICE_NAME,
- "createBulkMonitors"
- );
- /**
- * Checks if the endpoint can be resolved
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.query - The query parameters of the request.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message, and the resolution result.
- * @throws {Error} If there is an error during the process, especially if there is a validation error (422).
- */
- checkEndpointResolution = asyncHandler(
- async (req, res, next) => {
- await getMonitorURLByQueryValidation.validateAsync(req.query);
- const { monitorURL } = req.query;
- const parsedUrl = new URL(monitorURL);
- const response = await axios.get(parsedUrl, {
- timeout: 5000,
- validateStatus: () => true,
- });
- return res.success({
- status: response.status,
- msg: response.statusText,
- });
- },
- SERVICE_NAME,
- "checkEndpointResolution"
- );
-
- /**
- * Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.params - The parameters of the request.
- * @property {string} req.params.monitorId - The ID of the monitor to be deleted.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status and a message indicating the deletion of the monitor.
- * @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
- */
- deleteMonitor = asyncHandler(
- async (req, res, next) => {
- await getMonitorByIdParamValidation.validateAsync(req.params);
- const monitorId = req.params.monitorId;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- await this.verifyTeamAccess(teamId, monitorId);
-
- const monitor = await this.db.deleteMonitor({ monitorId });
- await this.jobQueue.deleteJob(monitor);
- await this.db.deleteStatusPagesByMonitorId(monitor._id);
- return res.success({ msg: this.stringService.monitorDelete });
- },
- SERVICE_NAME,
- "deleteMonitor"
- );
-
- /**
- * Deletes all monitors associated with a team.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.headers - The headers of the request.
- * @property {string} req.headers.authorization - The authorization header containing the JWT token.
- * @param {Object} res - The Express response object.
- * @param {function} next
- * @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
- * @throws {Error} If there is an error during the deletion process.
- */
- deleteAllMonitors = asyncHandler(
- async (req, res, next) => {
- const teamId = req?.user?.teamId;
- const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
- await Promise.all(
- monitors.map(async (monitor) => {
- try {
- await this.jobQueue.deleteJob(monitor);
- await this.db.deleteChecks(monitor._id);
- await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
- await this.db.deleteNotificationsByMonitorId(monitor._id);
- } catch (error) {
- logger.warn({
- message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
- service: SERVICE_NAME,
- method: "deleteAllMonitors",
- stack: error.stack,
- });
- }
- })
- );
- return res.success({ msg: `Deleted ${deletedCount} monitors` });
- },
- SERVICE_NAME,
- "deleteAllMonitors"
- );
-
- /**
- * Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.params - The parameters of the request.
- * @property {string} req.params.monitorId - The ID of the monitor to be edited.
- * @property {Object} req.body - The body of the request.
- * @property {Array} req.body.notifications - The notifications to be associated with the monitor.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data.
- * @throws {Error} If there is an error during the process, especially if there is a validation error (422).
- */
- editMonitor = asyncHandler(
- async (req, res, next) => {
- await getMonitorByIdParamValidation.validateAsync(req.params);
- await editMonitorBodyValidation.validateAsync(req.body);
- const monitorId = req?.params?.monitorId;
-
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- await this.verifyTeamAccess(teamId, monitorId);
-
- const editedMonitor = await this.db.editMonitor(monitorId, req.body);
-
- await this.jobQueue.updateJob(editedMonitor);
-
- return res.success({
- msg: this.stringService.monitorEdit,
- data: editedMonitor,
- });
- },
- SERVICE_NAME,
- "editMonitor"
- );
-
- /**
- * Pauses or resumes a monitor based on its current state.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.params - The parameters of the request.
- * @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data.
- * @throws {Error} If there is an error during the process.
- */
- pauseMonitor = asyncHandler(
- async (req, res, next) => {
- await pauseMonitorParamValidation.validateAsync(req.params);
-
- const monitorId = req.params.monitorId;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new Error("Team ID is required");
- }
-
- await this.verifyTeamAccess(teamId, monitorId);
-
- const monitor = await this.db.pauseMonitor({ monitorId });
- monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
-
- return res.success({
- msg: monitor.isActive ? this.stringService.monitorResume : this.stringService.monitorPause,
- data: monitor,
- });
- },
- SERVICE_NAME,
- "pauseMonitor"
- );
-
- /**
- * Adds demo monitors for a team.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.headers - The headers of the request.
- * @property {string} req.headers.authorization - The authorization header containing the JWT token.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added.
- * @throws {Error} If there is an error during the process.
- */
- addDemoMonitors = asyncHandler(
- async (req, res, next) => {
- const { _id, teamId } = req.user;
- const demoMonitors = await this.db.addDemoMonitors(_id, teamId);
- await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
-
- return res.success({
- msg: this.stringService.monitorDemoAdded,
- data: demoMonitors.length,
- });
- },
- SERVICE_NAME,
- "addDemoMonitors"
- );
-
- /**
- * Sends a test email to verify email delivery functionality.
- * @async
- * @param {Object} req - The Express request object.
- * @property {Object} req.body - The body of the request.
- * @property {string} req.body.to - The email address to send the test email to.
- * @param {Object} res - The Express response object.
- * @param {function} next - The next middleware function.
- * @returns {Object} The response object with a success status and the email delivery message ID.
- * @throws {Error} If there is an error while sending the test email.
- */
- sendTestEmail = asyncHandler(
- async (req, res, next) => {
- const { to } = req.body;
- if (!to || typeof to !== "string") {
- throw new Error(this.stringService.errorForValidEmailAddress);
- }
-
- const subject = this.stringService.testEmailSubject;
- const context = { testName: "Monitoring System" };
-
- const html = await this.emailService.buildEmail("testEmailTemplate", context);
- const messageId = await this.emailService.sendEmail(to, subject, html);
-
- if (!messageId) {
- throw createServerError("Failed to send test email.");
- }
-
- return res.success({
- msg: this.stringService.sendTestEmail,
- data: { messageId },
- });
- },
- SERVICE_NAME,
- "sendTestEmail"
- );
-
- getMonitorsByTeamId = asyncHandler(
- async (req, res, next) => {
- await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
- await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
-
- let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
- const teamId = req.user.teamId;
-
- const monitors = await this.db.getMonitorsByTeamId({
- limit,
- type,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- teamId,
- });
- return res.success({
- msg: this.stringService.monitorGetByTeamId,
- data: monitors,
- });
- },
- SERVICE_NAME,
- "getMonitorsByTeamId"
- );
-
- getMonitorsAndSummaryByTeamId = asyncHandler(
- async (req, res, next) => {
- await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
- await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
-
- const { explain } = req;
- const { type } = req.query;
- const { teamId } = req.user;
-
- const result = await this.db.getMonitorsAndSummaryByTeamId({
- type,
- explain,
- teamId,
- });
- return res.success({
- msg: "OK", // TODO
- data: result,
- });
- },
- SERVICE_NAME,
- "getMonitorsAndSummaryByTeamId"
- );
-
- getMonitorsWithChecksByTeamId = asyncHandler(
- async (req, res, next) => {
- await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
- await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
-
- const { explain } = req;
- let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
- const { teamId } = req.user;
-
- const result = await this.db.getMonitorsWithChecksByTeamId({
- limit,
- type,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- teamId,
- explain,
- });
- return res.success({
- msg: "OK",
- data: result,
- });
- },
- SERVICE_NAME,
- "getMonitorsWithChecksByTeamId"
- );
-
- seedDb = asyncHandler(
- async (req, res, next) => {
- const { _id, teamId } = req.user;
- await seedDb(_id, teamId);
- res.success({ msg: "Database seeded" });
- },
- SERVICE_NAME,
- "seedDb"
- );
-
- exportMonitorsToCSV = asyncHandler(
- async (req, res, next) => {
- const { teamId } = req.user;
-
- const monitors = await this.db.getMonitorsByTeamId({ teamId });
- if (!monitors || monitors.length === 0) {
- return res.success({
- msg: this.stringService.noMonitorsFound,
- data: null,
- });
- }
- const csvData = monitors?.filteredMonitors?.map((monitor) => ({
- name: monitor.name,
- description: monitor.description,
- type: monitor.type,
- url: monitor.url,
- interval: monitor.interval,
- port: monitor.port,
- ignoreTlsErrors: monitor.ignoreTlsErrors,
- isActive: monitor.isActive,
- }));
-
- const csv = pkg.unparse(csvData);
-
- return res.file({
- data: csv,
- headers: {
- "Content-Type": "text/csv",
- "Content-Disposition": "attachment; filename=monitors.csv",
- },
- });
- },
- SERVICE_NAME,
- "exportMonitorsToCSV"
- );
-}
-
-export default MonitorController;
diff --git a/server/index.js b/server/index.js
old mode 100755
new mode 100644
index 9c7778a99..5c835b9f6
--- a/server/index.js
+++ b/server/index.js
@@ -1,428 +1,47 @@
+import { initializeServices } from "./src/config/services.js";
+import { initializeControllers } from "./src/config/controllers.js";
+import { createApp } from "./src/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 express from "express";
-import helmet from "helmet";
-import cors from "cors";
-import compression from "compression";
-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 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";
-
-// 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";
-
-//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";
+import SettingsService from "./src/service/system/settingsService.js";
+import AppSettings from "./src/db/models/AppSettings.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 });
-
- // Business services
- const userService = new UserService({
- db,
- emailService,
- settingsService,
- logger,
- stringService,
- jwt,
- });
- const checkService = new CheckService({
- db,
- settingsService,
- stringService,
- });
- const diagnosticService = new DiagnosticService();
- const inviteService = new InviteService({
- db,
- settingsService,
- emailService,
- stringService,
- });
- const maintenanceWindowService = new MaintenanceWindowService({
- db,
- settingsService,
- stringService,
- });
- // 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,
});
- // Register services
- // ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
- // ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue);
- ServiceRegistry.register(JobQueue.SERVICE_NAME, superSimpleQueue);
- 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);
-
- 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 authController = new AuthController({
- db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
- 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(
- ServiceRegistry.get(MongoDB.SERVICE_NAME),
- ServiceRegistry.get(SettingsService.SERVICE_NAME),
- ServiceRegistry.get(JobQueue.SERVICE_NAME),
- ServiceRegistry.get(StringService.SERVICE_NAME),
- ServiceRegistry.get(EmailService.SERVICE_NAME)
- );
-
- const settingsController = new SettingsController({
- db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
- 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),
- 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 maintenanceWindowController = new MaintenanceWindowController({
- db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
- 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 diagnosticController = new DiagnosticController({
- diagnosticService,
- });
-
- //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/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/status-page", statusPageRoutes.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/nodemon.json b/server/nodemon.json
index 928d1abc2..88d6ac686 100755
--- a/server/nodemon.json
+++ b/server/nodemon.json
@@ -1,5 +1,5 @@
{
- "ignore": ["locales/*", "*.log", "node_modules/*"],
- "watch": ["*.js", "*.json"],
+ "ignore": ["src/locales/*", "*.log", "node_modules/*"],
+ "watch": ["src/**/*.js", "*.json"],
"ext": "js,json"
}
diff --git a/server/package.json b/server/package.json
index 62a91921b..d3d7344e9 100755
--- a/server/package.json
+++ b/server/package.json
@@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"test": "c8 mocha",
- "dev": "nodemon index.js",
+ "dev": "nodemon src/index.js",
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"format": "prettier --write .",
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/src/app.js b/server/src/app.js
new file mode 100644
index 000000000..af53dbf56
--- /dev/null
+++ b/server/src/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/src/config/controllers.js b/server/src/config/controllers.js
new file mode 100644
index 000000000..693e35ab8
--- /dev/null
+++ b/server/src/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.errorService, services.logger, 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/src/config/database.js b/server/src/config/database.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/src/config/routes.js b/server/src/config/routes.js
new file mode 100644
index 000000000..64b1df474
--- /dev/null
+++ b/server/src/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/src/config/services.js b/server/src/config/services.js
new file mode 100644
index 000000000..75936c29e
--- /dev/null
+++ b/server/src/config/services.js
@@ -0,0 +1,155 @@
+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";
+import crypto from "crypto";
+
+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({
+ crypto,
+ db,
+ emailService,
+ settingsService,
+ logger,
+ stringService,
+ jwt,
+ errorService,
+ jobQueue: superSimpleQueue,
+ });
+ 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/src/controllers/announcementsController.js
similarity index 88%
rename from server/controllers/announcementsController.js
rename to server/src/controllers/announcementsController.js
index b17b8eb91..430859791 100755
--- a/server/controllers/announcementsController.js
+++ b/server/src/controllers/announcementsController.js
@@ -1,5 +1,5 @@
import { createAnnouncementValidation } from "../validation/joi.js";
-import { asyncHandler } from "../utils/errorUtils.js";
+import BaseController from "./baseController.js";
const SERVICE_NAME = "announcementController";
@@ -10,14 +10,18 @@ const SERVICE_NAME = "announcementController";
* @class AnnouncementController
*/
-class AnnouncementController {
- constructor(db, stringService) {
- this.db = db;
- this.stringService = stringService;
+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/src/controllers/authController.js
similarity index 94%
rename from server/controllers/authController.js
rename to server/src/controllers/authController.js
index 12d2f77f4..c186e8423 100755
--- a/server/controllers/authController.js
+++ b/server/src/controllers/authController.js
@@ -1,3 +1,4 @@
+import BaseController from "./baseController.js";
import {
registrationBodyValidation,
loginValidation,
@@ -10,7 +11,6 @@ import {
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
} from "../validation/joi.js";
-import { asyncHandler, createError } from "../utils/errorUtils.js";
const SERVICE_NAME = "authController";
@@ -23,29 +23,30 @@ const SERVICE_NAME = "authController";
* @class AuthController
* @description Manages user authentication and authorization operations
*/
-class AuthController {
+class AuthController extends BaseController {
+ static SERVICE_NAME = SERVICE_NAME;
/**
* 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;
}
+ get serviceName() {
+ return AuthController.SERVICE_NAME;
+ }
+
/**
* Registers a new user in the system.
*
@@ -85,7 +86,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 +122,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 +174,7 @@ class AuthController {
* "newPassword": "NewPass123!"
* }
*/
- editUser = asyncHandler(
+ editUser = this.asyncHandler(
async (req, res) => {
await editUserBodyValidation.validateAsync(req.body);
@@ -200,7 +201,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 +231,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 +263,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 +295,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 +327,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 +351,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 +382,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 +432,7 @@ class AuthController {
* }
* // Requires JWT authentication with superadmin role
*/
- editUserById = asyncHandler(
+ editUserById = this.asyncHandler(
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
diff --git a/server/src/controllers/baseController.js b/server/src/controllers/baseController.js
new file mode 100644
index 000000000..910cf57b4
--- /dev/null
+++ b/server/src/controllers/baseController.js
@@ -0,0 +1,83 @@
+import { AppError } from "../service/infrastructure/errorService.js";
+
+export const createCommonDependencies = (db, errorService, logger, stringService) => {
+ return {
+ db,
+ errorService,
+ logger,
+ stringService,
+ };
+};
+
+class BaseController {
+ constructor({ db, logger, errorService, ...additionalDependencies }) {
+ this.db = db;
+ this.logger = logger;
+ this.errorService = errorService;
+ Object.assign(this, additionalDependencies);
+
+ this.asyncHandler = (fn, serviceName, methodName) => {
+ return async (req, res, next) => {
+ try {
+ await fn(req, res, next);
+ } catch (error) {
+ // Handle validation errors
+ if (error.isJoi) {
+ const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName);
+ return next(validationError);
+ }
+
+ if (error.name === "ValidationError") {
+ const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName);
+ return next(validationError);
+ }
+
+ if (error.name === "CastError") {
+ const notFoundError = this.errorService.createNotFoundError(
+ "Invalid resource identifier",
+ { field: error.path, value: error.value },
+ serviceName,
+ methodName
+ );
+ return next(notFoundError);
+ }
+
+ if (error.code === "11000") {
+ const conflictError = this.errorService.createConflictError("Resource already exists", {
+ originalError: error.message,
+ code: error.code,
+ });
+ conflictError.service = serviceName;
+ conflictError.method = methodName;
+ return next(conflictError);
+ }
+
+ if (error instanceof AppError) {
+ error.service = error.service || serviceName;
+ error.method = error.method || methodName;
+ return next(error);
+ }
+
+ if (error.status) {
+ const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, {
+ originalError: error.message,
+ stack: error.stack,
+ });
+ return next(appError);
+ }
+
+ // For unknown errors, create a server error
+ const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", {
+ originalError: error.message,
+ stack: error.stack,
+ });
+ appError.service = serviceName;
+ appError.method = methodName;
+ appError.stack = error.stack; // Preserve original stack
+ return next(appError);
+ }
+ };
+ };
+ }
+}
+export default BaseController;
diff --git a/server/controllers/checkController.js b/server/src/controllers/checkController.js
similarity index 94%
rename from server/controllers/checkController.js
rename to server/src/controllers/checkController.js
index 304f89c6d..4069df3ff 100755
--- a/server/controllers/checkController.js
+++ b/server/src/controllers/checkController.js
@@ -1,3 +1,4 @@
+import BaseController from "./baseController.js";
import {
getChecksParamValidation,
getChecksQueryValidation,
@@ -9,7 +10,6 @@ import {
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
} from "../validation/joi.js";
-import { asyncHandler } from "../utils/errorUtils.js";
const SERVICE_NAME = "checkController";
@@ -22,23 +22,26 @@ const SERVICE_NAME = "checkController";
* @class CheckController
* @description Manages check operations and monitoring data
*/
-class CheckController {
+class CheckController extends BaseController {
+ static SERVICE_NAME = SERVICE_NAME;
/**
* 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;
}
+ get serviceName() {
+ return CheckController.SERVICE_NAME;
+ }
+
/**
* Retrieves checks for a specific monitor with filtering and pagination.
*
@@ -67,7 +70,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 +112,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 +143,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 +179,7 @@ class CheckController {
* }
* // Requires JWT authentication
*/
- ackCheck = asyncHandler(
+ ackCheck = this.asyncHandler(
async (req, res) => {
await ackCheckBodyValidation.validateAsync(req.body);
@@ -220,7 +223,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 +269,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 +303,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 +339,7 @@ class CheckController {
* // Requires JWT authentication
* // Sets check TTL to 30 days
*/
- updateChecksTTL = asyncHandler(
+ updateChecksTTL = this.asyncHandler(
async (req, res) => {
await updateChecksTTLBodyValidation.validateAsync(req.body);
diff --git a/server/controllers/controllerUtils.js b/server/src/controllers/controllerUtils.js
similarity index 76%
rename from server/controllers/controllerUtils.js
rename to server/src/controllers/controllerUtils.js
index 2b3a4198c..f21ba154b 100755
--- a/server/controllers/controllerUtils.js
+++ b/server/src/controllers/controllerUtils.js
@@ -1,12 +1,10 @@
-import { createServerError } from "../utils/errorUtils.js";
-
const fetchMonitorCertificate = async (sslChecker, monitor) => {
const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname;
const cert = await sslChecker(hostname);
// Throw an error if no cert or if cert.validTo is not present
if (cert?.validTo === null || cert?.validTo === undefined) {
- throw createServerError("Certificate not found");
+ throw new Error("Certificate not found");
}
return cert;
};
diff --git a/server/controllers/diagnosticController.js b/server/src/controllers/diagnosticController.js
similarity index 78%
rename from server/controllers/diagnosticController.js
rename to server/src/controllers/diagnosticController.js
index 585684211..0723a9f6c 100755
--- a/server/controllers/diagnosticController.js
+++ b/server/src/controllers/diagnosticController.js
@@ -1,6 +1,5 @@
-import { asyncHandler } from "../utils/errorUtils.js";
-
const SERVICE_NAME = "diagnosticController";
+import BaseController from "./baseController.js";
/**
* Diagnostic Controller
*
@@ -10,17 +9,22 @@ const SERVICE_NAME = "diagnosticController";
* @class DiagnosticController
* @description Manages system diagnostics and performance monitoring
*/
-class DiagnosticController {
+class DiagnosticController extends BaseController {
+ static SERVICE_NAME = SERVICE_NAME;
/**
* 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;
}
+ get serviceName() {
+ return DiagnosticController.SERVICE_NAME;
+ }
/**
* Retrieves comprehensive system statistics and performance metrics.
*
@@ -41,7 +45,7 @@ class DiagnosticController {
* // - Database connection status
* // - Active processes/connections
*/
- getSystemStats = asyncHandler(
+ getSystemStats = this.asyncHandler(
async (req, res) => {
const diagnostics = await this.diagnosticService.getSystemStats();
return res.success({
diff --git a/server/controllers/inviteController.js b/server/src/controllers/inviteController.js
similarity index 85%
rename from server/controllers/inviteController.js
rename to server/src/controllers/inviteController.js
index 75e98cb68..a377234c0 100755
--- a/server/controllers/inviteController.js
+++ b/server/src/controllers/inviteController.js
@@ -1,24 +1,27 @@
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 {
+ static SERVICE_NAME = SERVICE_NAME;
/**
* 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;
}
+ get serviceName() {
+ return InviteController.SERVICE_NAME;
+ }
+
/**
* Generates an invite token for a user invitation
* @param {Object} req - Express request object
@@ -28,7 +31,7 @@ class InviteController {
* @param {Object} res - Express response object
* @returns {Promise