Merge remote-tracking branch 'origin/develop' into improve/light-mode-ui

This commit is contained in:
karenvicent committed 2025-07-28 16:33:20 -04:00
commit 261c83158b
134 files changed
+1999 -1745

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![OpenSSF Mejores Prácticas](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
[![Pregunta en DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad</strong></p>
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
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:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
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:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## 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
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
## 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.
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Historial de estrellas](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](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.
+7 -7
View File
@@ -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";
}, []);
+24 -24
View File
@@ -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}
</Typography>
<Stack
direction="row"
@@ -267,7 +267,7 @@ const Configure = () => {
component="h2"
variant="monitorUrl"
>
{form.url?.replace(/^https?:\/\//, "") || "..."}
{form?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
@@ -352,11 +352,11 @@ const Configure = () => {
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"}
/>
<TextInput
name="name"
@@ -379,7 +379,7 @@ const Configure = () => {
<NotificationsConfig
notifications={notifications}
setMonitor={setForm}
setNotifications={form.notifications}
setNotifications={form?.notifications}
/>
</ConfigBox>
<ConfigBox>
@@ -398,7 +398,7 @@ const Configure = () => {
control={
<Switch
name="ignoreTlsErrors"
checked={form.ignoreTlsErrors ?? false}
checked={form?.ignoreTlsErrors ?? false}
onChange={onChange}
sx={{ mr: theme.spacing(2) }}
/>
@@ -430,12 +430,12 @@ const Configure = () => {
isChecked={useAdvancedMatching}
onChange={onChange}
/>
{form.type === "http" && useAdvancedMatching && (
{form?.type === "http" && useAdvancedMatching && (
<>
<Select
name="matchMethod"
label={t("matchMethod")}
value={form.matchMethod || "equal"}
value={form?.matchMethod || "equal"}
onChange={onChange}
items={matchMethodOptions}
/>
@@ -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"]}
-24
View File
@@ -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<AxiosResponse>} 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
+1 -1
View File
@@ -10,4 +10,4 @@ COPY ./server/ ./
EXPOSE 52345
CMD ["node", "index.js"]
CMD ["node", "src/index.js"]
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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..."
+1 -1
View File
@@ -10,4 +10,4 @@ COPY ./server/ ./
EXPOSE 52345
CMD ["node", "index.js"]
CMD ["node", "src/index.js"]
+1 -1
View File
@@ -12,4 +12,4 @@ COPY ./server ./
EXPOSE 52345
CMD ["node", "index.js"]
CMD ["node", "src/index.js"]
+1 -1
View File
@@ -12,4 +12,4 @@ COPY ./server ./
EXPOSE 52345
CMD ["node", "index.js"]
CMD ["node", "src/index.js"]
-22
View File
@@ -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;
-708
View File
@@ -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<Express.Response>}
* @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<Express.Response>}
* @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<Express.Response>}
* @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;
Executable → Regular
+24 -405
View File
@@ -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) => {
+2 -2
View File
@@ -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"
}
+1 -1
View File
@@ -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 .",
-208
View File
@@ -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;
+73
View File
@@ -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;
};
+66
View File
@@ -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;
};
View File
Whitespace-only changes.
+39
View File
@@ -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());
};
+155
View File
@@ -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;
};
@@ -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.
*
@@ -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")) {
+83
View File
@@ -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;
@@ -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);
@@ -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;
};
@@ -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({
@@ -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<Object>} Response with invite token data
*/
getInviteToken = asyncHandler(
getInviteToken = this.asyncHandler(
async (req, res) => {
const invite = req.body;
const teamId = req?.user?.teamId;
@@ -54,7 +57,7 @@ class InviteController {
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with invite token data
*/
sendInviteEmail = asyncHandler(
sendInviteEmail = this.asyncHandler(
async (req, res) => {
const inviteRequest = req.body;
inviteRequest.teamId = req?.user?.teamId;
@@ -81,7 +84,7 @@ class InviteController {
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with verified invite data
*/
verifyInviteToken = asyncHandler(
verifyInviteToken = this.asyncHandler(
async (req, res) => {
await inviteVerificationBodyValidation.validateAsync(req.body);
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
+26
View File
@@ -0,0 +1,26 @@
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();
res.success({
msg: "Logs fetched successfully",
data: logs,
});
},
SERVICE_NAME,
"getLogs"
);
}
export default LogController;
@@ -7,25 +7,29 @@ import {
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "maintenanceWindowController";
class MaintenanceWindowController {
constructor({ db, settingsService, stringService, maintenanceWindowService }) {
this.db = db;
class MaintenanceWindowController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
constructor(commonDependencies, { settingsService, maintenanceWindowService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.maintenanceWindowService = maintenanceWindowService;
}
createMaintenanceWindows = asyncHandler(
get serviceName() {
return MaintenanceWindowController.SERVICE_NAME;
}
createMaintenanceWindows = this.asyncHandler(
async (req, res) => {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
@@ -38,13 +42,13 @@ class MaintenanceWindowController {
"createMaintenanceWindows"
);
getMaintenanceWindowById = asyncHandler(
getMaintenanceWindowById = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req.user.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId });
@@ -58,14 +62,14 @@ class MaintenanceWindowController {
"getMaintenanceWindowById"
);
getMaintenanceWindowsByTeamId = asyncHandler(
getMaintenanceWindowsByTeamId = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
@@ -79,13 +83,13 @@ class MaintenanceWindowController {
"getMaintenanceWindowsByTeamId"
);
getMaintenanceWindowsByMonitorId = asyncHandler(
getMaintenanceWindowsByMonitorId = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId });
@@ -99,13 +103,13 @@ class MaintenanceWindowController {
"getMaintenanceWindowsByMonitorId"
);
deleteMaintenanceWindow = asyncHandler(
deleteMaintenanceWindow = this.asyncHandler(
async (req, res) => {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId });
@@ -118,14 +122,14 @@ class MaintenanceWindowController {
"deleteMaintenanceWindow"
);
editMaintenanceWindow = asyncHandler(
editMaintenanceWindow = this.asyncHandler(
async (req, res) => {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
const teamId = req.user.teamId;
if (!teamId) {
throw new Error("Team ID is required");
throw this.errorService.createBadRequestError("Team ID is required");
}
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId });
+446
View File
@@ -0,0 +1,446 @@
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
createMonitorBodyValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
getCertificateParamValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
} from "../validation/joi.js";
import sslChecker from "ssl-checker";
import { fetchMonitorCertificate } from "./controllerUtils.js";
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;
this.jobQueue = jobQueue;
this.emailService = emailService;
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)) {
throw this.errorService.createAuthorizationError();
}
}
getAllMonitors = this.asyncHandler(
async (req, res) => {
const monitors = await this.monitorService.getAllMonitors();
return res.success({
msg: this.stringService.monitorGetAll,
data: monitors,
});
},
SERVICE_NAME,
"getAllMonitors"
);
getUptimeDetailsById = this.asyncHandler(
async (req, res) => {
const monitorId = req?.params?.monitorId;
const dateRange = req?.query?.dateRange;
const normalize = req?.query?.normalize;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const data = await this.monitorService.getUptimeDetailsById({
teamId,
monitorId,
dateRange,
normalize,
});
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data: data,
});
},
SERVICE_NAME,
"getUptimeDetailsById"
);
getMonitorStatsById = this.asyncHandler(
async (req, res) => {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
const monitorId = req?.params?.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitorStats = await this.monitorService.getMonitorStatsById({
teamId,
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<Express.Response>}
* @throws {Error} - Throws error if monitor not found or other database errors
*/
getHardwareDetailsById = this.asyncHandler(
async (req, res) => {
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 this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.getHardwareDetailsById({
teamId,
monitorId,
dateRange,
});
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data: monitor,
});
},
SERVICE_NAME,
"getHardwareDetailsById"
);
getMonitorCertificate = this.asyncHandler(
async (req, res) => {
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"
);
getMonitorById = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data: monitor,
});
},
SERVICE_NAME,
"getMonitorById"
);
createMonitor = this.asyncHandler(
async (req, res) => {
await createMonitorBodyValidation.validateAsync(req.body);
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body });
return res.success({
msg: this.stringService.monitorCreate,
data: monitor,
});
},
SERVICE_NAME,
"createMonitor"
);
createBulkMonitors = this.asyncHandler(
async (req, res) => {
if (!req.file) {
throw this.errorService.createBadRequestError("No file uploaded");
}
if (!req.file.mimetype.includes("csv")) {
throw this.errorService.createBadRequestError("File is not a CSV");
}
if (req.file.size === 0) {
throw this.errorService.createBadRequestError("File is empty");
}
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
if (!userId || !teamId) {
throw this.errorService.createBadRequestError("Missing userId or teamId");
}
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
throw this.errorService.createBadRequestError("Cannot get file from buffer");
}
const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId });
return res.success({
msg: this.stringService.bulkMonitorsCreate,
data: monitors,
});
},
SERVICE_NAME,
"createBulkMonitors"
);
deleteMonitor = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
const monitorId = req.params.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
return res.success({ msg: this.stringService.monitorDelete, data: deletedMonitor });
},
SERVICE_NAME,
"deleteMonitor"
);
deleteAllMonitors = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
return res.success({ msg: `Deleted ${deletedCount} monitors` });
},
SERVICE_NAME,
"deleteAllMonitors"
);
editMonitor = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
const monitorId = req?.params?.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
return res.success({
msg: this.stringService.monitorEdit,
data: editedMonitor,
});
},
SERVICE_NAME,
"editMonitor"
);
pauseMonitor = this.asyncHandler(
async (req, res) => {
await pauseMonitorParamValidation.validateAsync(req.params);
const monitorId = req.params.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
return res.success({
msg: monitor.isActive ? this.stringService.monitorResume : this.stringService.monitorPause,
data: monitor,
});
},
SERVICE_NAME,
"pauseMonitor"
);
addDemoMonitors = this.asyncHandler(
async (req, res) => {
const { _id, teamId } = req.user;
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
return res.success({
msg: this.stringService.monitorDemoAdded,
data: demoMonitors?.length ?? 0,
});
},
SERVICE_NAME,
"addDemoMonitors"
);
sendTestEmail = this.asyncHandler(
async (req, res) => {
const { to } = req.body;
if (!to || typeof to !== "string") {
throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress);
}
const messageId = await this.monitorService.sendTestEmail({ to });
return res.success({
msg: this.stringService.sendTestEmail,
data: { messageId },
});
},
SERVICE_NAME,
"sendTestEmail"
);
getMonitorsByTeamId = this.asyncHandler(
async (req, res) => {
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.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order });
return res.success({
msg: this.stringService.monitorGetByTeamId,
data: monitors,
});
},
SERVICE_NAME,
"getMonitorsByTeamId"
);
getMonitorsAndSummaryByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
const explain = req?.query?.explain;
const type = req?.query?.type;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
return res.success({
msg: "OK", // TODO
data: result,
});
},
SERVICE_NAME,
"getMonitorsAndSummaryByTeamId"
);
getMonitorsWithChecksByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
const explain = req?.query?.explain;
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId,
limit,
type,
page,
rowsPerPage,
filter,
field,
order,
explain,
});
return res.success({
msg: "OK",
data: monitors,
});
},
SERVICE_NAME,
"getMonitorsWithChecksByTeamId"
);
exportMonitorsToCSV = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const csv = await this.monitorService.exportMonitorsToCSV({ teamId });
return res.file({
data: csv,
headers: {
"Content-Type": "text/csv",
"Content-Disposition": "attachment; filename=monitors.csv",
},
});
},
SERVICE_NAME,
"exportMonitorsToCSV"
);
}
export default MonitorController;
@@ -1,27 +1,28 @@
import { createNotificationBodyValidation } from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "NotificationController";
class NotificationController {
constructor({ notificationService, stringService, statusService, db }) {
class NotificationController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
constructor(commonDependencies, { notificationService, statusService }) {
super(commonDependencies);
this.notificationService = notificationService;
this.stringService = stringService;
this.statusService = statusService;
this.db = db;
}
testNotification = asyncHandler(
async (req, res, next) => {
get serviceName() {
return NotificationController.SERVICE_NAME;
}
testNotification = this.asyncHandler(
async (req, res) => {
const notification = req.body;
const success = await this.notificationService.sendTestNotification(notification);
if (!success) {
return res.error({
msg: "Sending notification failed",
status: 400,
});
throw this.errorService.createServerError("Sending notification failed");
}
return res.success({
@@ -32,8 +33,8 @@ class NotificationController {
"testNotification"
);
createNotification = asyncHandler(
async (req, res, next) => {
createNotification = this.asyncHandler(
async (req, res) => {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
@@ -42,12 +43,12 @@ class NotificationController {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const userId = req?.user?._id;
if (!userId) {
throw new Error("No user ID in request");
throw this.errorService.createBadRequestError("User ID is required");
}
body.userId = userId;
body.teamId = teamId;
@@ -62,11 +63,11 @@ class NotificationController {
"createNotification"
);
getNotificationsByTeamId = asyncHandler(
async (req, res, next) => {
getNotificationsByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const notifications = await this.db.getNotificationsByTeamId(teamId);
@@ -80,18 +81,16 @@ class NotificationController {
"getNotificationsByTeamId"
);
deleteNotification = asyncHandler(
async (req, res, next) => {
deleteNotification = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
await this.db.deleteNotificationById(req.params.id);
@@ -103,19 +102,17 @@ class NotificationController {
"deleteNotification"
);
getNotificationById = asyncHandler(
async (req, res, next) => {
getNotificationById = this.asyncHandler(
async (req, res) => {
const notification = await this.db.getNotificationById(req.params.id);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
if (!notification.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
return res.success({
msg: "Notification fetched successfully",
@@ -126,23 +123,21 @@ class NotificationController {
"getNotificationById"
);
editNotification = asyncHandler(
async (req, res, next) => {
editNotification = this.asyncHandler(
async (req, res) => {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
const editedNotification = await this.db.editNotification(req.params.id, req.body);
@@ -155,26 +150,24 @@ class NotificationController {
"editNotification"
);
testAllNotifications = asyncHandler(
async (req, res, next) => {
testAllNotifications = this.asyncHandler(
async (req, res) => {
const monitorId = req.body.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
const error = new Error("Unauthorized");
error.status = 403;
throw error;
throw this.errorService.createAuthorizationError();
}
const notifications = monitor.notifications;
if (notifications.length === 0) throw new Error("No notifications");
if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications");
const result = await this.notificationService.testAllNotifications(notifications);
if (!result) throw new Error("Failed to send all notifications");
if (!result) throw this.errorService.createServerError("Failed to send all notifications");
return res.success({
msg: "All notifications sent successfully",
});
@@ -1,15 +1,19 @@
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "JobQueueController";
class JobQueueController {
constructor(jobQueue, stringService) {
class JobQueueController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
constructor(commonDependencies, { jobQueue }) {
super(commonDependencies);
this.jobQueue = jobQueue;
this.stringService = stringService;
}
getMetrics = asyncHandler(
async (req, res, next) => {
get serviceName() {
return JobQueueController.SERVICE_NAME;
}
getMetrics = this.asyncHandler(
async (req, res) => {
const metrics = await this.jobQueue.getMetrics();
res.success({
msg: this.stringService.queueGetMetrics,
@@ -20,8 +24,8 @@ class JobQueueController {
"getMetrics"
);
getJobs = asyncHandler(
async (req, res, next) => {
getJobs = this.asyncHandler(
async (req, res) => {
const jobs = await this.jobQueue.getJobs();
return res.success({
msg: this.stringService.queueGetJobs,
@@ -32,8 +36,8 @@ class JobQueueController {
"getJobs"
);
getAllMetrics = asyncHandler(
async (req, res, next) => {
getAllMetrics = this.asyncHandler(
async (req, res) => {
const jobs = await this.jobQueue.getJobs();
const metrics = await this.jobQueue.getMetrics();
return res.success({
@@ -45,8 +49,8 @@ class JobQueueController {
"getAllMetrics"
);
addJob = asyncHandler(
async (req, res, next) => {
addJob = this.asyncHandler(
async (req, res) => {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.success({
msg: this.stringService.queueAddJob,
@@ -56,8 +60,8 @@ class JobQueueController {
"addJob"
);
flushQueue = asyncHandler(
async (req, res, next) => {
flushQueue = this.asyncHandler(
async (req, res) => {
const result = await this.jobQueue.flushQueues();
return res.success({
msg: this.stringService.jobQueueFlush,
@@ -68,8 +72,8 @@ class JobQueueController {
"flushQueue"
);
checkQueueHealth = asyncHandler(
async (req, res, next) => {
checkQueueHealth = this.asyncHandler(
async (req, res) => {
const stuckQueues = await this.jobQueue.checkQueueHealth();
return res.success({
msg: this.stringService.queueHealthCheck,
@@ -1,17 +1,21 @@
import { updateAppSettingsBodyValidation } from "../validation/joi.js";
import { sendTestEmailBodyValidation } from "../validation/joi.js";
import { asyncHandler, createServerError } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "SettingsController";
class SettingsController {
constructor({ db, settingsService, stringService, emailService }) {
this.db = db;
class SettingsController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
constructor(commonDependencies, { settingsService, emailService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.emailService = emailService;
}
get serviceName() {
return SettingsController.SERVICE_NAME;
}
buildAppSettings = (dbSettings) => {
const sanitizedSettings = { ...dbSettings };
delete sanitizedSettings.version;
@@ -33,8 +37,8 @@ class SettingsController {
return returnSettings;
};
getAppSettings = asyncHandler(
async (req, res, next) => {
getAppSettings = this.asyncHandler(
async (req, res) => {
const dbSettings = await this.settingsService.getDBSettings();
const returnSettings = this.buildAppSettings(dbSettings);
@@ -47,8 +51,8 @@ class SettingsController {
"getAppSettings"
);
updateAppSettings = asyncHandler(
async (req, res, next) => {
updateAppSettings = this.asyncHandler(
async (req, res) => {
await updateAppSettingsBodyValidation.validateAsync(req.body);
const updatedSettings = await this.db.updateAppSettings(req.body);
@@ -62,14 +66,9 @@ class SettingsController {
"updateAppSettings"
);
sendTestEmail = asyncHandler(
async (req, res, next) => {
try {
await sendTestEmailBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
sendTestEmail = this.asyncHandler(
async (req, res) => {
await sendTestEmailBodyValidation.validateAsync(req.body);
const {
to,
@@ -107,7 +106,7 @@ class SettingsController {
});
if (!messageId) {
throw createServerError("Failed to send test email.");
throw this.errorService.createServerError("Failed to send test email.");
}
return res.success({
@@ -1,16 +1,20 @@
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js";
import { asyncHandler } from "../utils/errorUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "statusPageController";
class StatusPageController {
constructor(db, stringService) {
this.db = db;
this.stringService = stringService;
class StatusPageController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
constructor(commonDependencies) {
super(commonDependencies);
}
createStatusPage = asyncHandler(
async (req, res, next) => {
get serviceName() {
return StatusPageController.SERVICE_NAME;
}
createStatusPage = this.asyncHandler(
async (req, res) => {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
@@ -30,16 +34,14 @@ class StatusPageController {
"createStatusPage"
);
updateStatusPage = asyncHandler(
async (req, res, next) => {
updateStatusPage = this.asyncHandler(
async (req, res) => {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
const statusPage = await this.db.updateStatusPage(req.body, req.file);
if (statusPage === null) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound);
}
return res.success({
msg: this.stringService.statusPageUpdate,
@@ -50,8 +52,8 @@ class StatusPageController {
"updateStatusPage"
);
getStatusPage = asyncHandler(
async (req, res, next) => {
getStatusPage = this.asyncHandler(
async (req, res) => {
const statusPage = await this.db.getStatusPage();
return res.success({
msg: this.stringService.statusPageByUrl,
@@ -62,8 +64,8 @@ class StatusPageController {
"getStatusPage"
);
getStatusPageByUrl = asyncHandler(
async (req, res, next) => {
getStatusPageByUrl = this.asyncHandler(
async (req, res) => {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
@@ -77,8 +79,8 @@ class StatusPageController {
"getStatusPageByUrl"
);
getStatusPagesByTeamId = asyncHandler(
async (req, res, next) => {
getStatusPagesByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req.user.teamId;
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
@@ -91,8 +93,8 @@ class StatusPageController {
"getStatusPagesByTeamId"
);
deleteStatusPage = asyncHandler(
async (req, res, next) => {
deleteStatusPage = this.asyncHandler(
async (req, res) => {
await this.db.deleteStatusPage(req.params.url);
return res.success({
msg: this.stringService.statusPageDelete,
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -126,18 +126,22 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
try {
const doc = await this.model.findOne(this.getFilter());
if (doc.type === "pagespeed") {
if (!doc) {
throw new Error("Monitor not found");
}
if (doc?.type === "pagespeed") {
await PageSpeedCheck.deleteMany({ monitorId: doc._id });
} else if (doc.type === "hardware") {
} else if (doc?.type === "hardware") {
await HardwareCheck.deleteMany({ monitorId: doc._id });
} else {
await Check.deleteMany({ monitorId: doc._id });
}
// Deal with status pages
await StatusPage.updateMany({ monitors: doc._id }, { $pull: { monitors: doc._id } });
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
await MonitorStats.deleteMany({ monitorId: doc._id.toString() });
await MonitorStats.deleteMany({ monitorId: doc?._id.toString() });
next();
} catch (error) {
next(error);
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,5 @@
import mongoose from "mongoose";
import { BaseCheckSchema } from "./Check.js";
import logger from "../../utils/logger.js";
import { time } from "console";
const AuditSchema = mongoose.Schema({
id: { type: String, required: true },
title: { type: String, required: true },
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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";
@@ -279,7 +279,6 @@ const calculateGroupStats = (group) => {
* @throws {Error}
*/
const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const dates = getDateRange(dateRange);
const formatLookup = {
@@ -595,16 +594,16 @@ const createBulkMonitors = async (req) => {
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const deleteMonitor = async ({ monitorId }) => {
const deleteMonitor = async ({ teamId, monitorId }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const monitor = await Monitor.findByIdAndDelete(monitorId);
const deletedMonitor = await Monitor.findOneAndDelete({ _id: monitorId, teamId });
if (!monitor) {
if (!deletedMonitor) {
throw new Error(stringService.getDbFindMonitorById(monitorId));
}
return monitor;
return deletedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitor";
@@ -654,9 +653,9 @@ const deleteMonitorsByUserId = async (userId) => {
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const editMonitor = async (candidateId, candidateMonitor) => {
const editMonitor = async ({ monitorId, body }) => {
try {
const editedMonitor = await Monitor.findByIdAndUpdate(candidateId, candidateMonitor, {
const editedMonitor = await Monitor.findByIdAndUpdate(monitorId, body, {
new: true,
});
return editedMonitor;
@@ -138,6 +138,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
{
$project: {
_id: 1,
teamId: 1,
name: 1,
status: 1,
interval: 1,
@@ -135,6 +135,14 @@ const getStatusPage = async (url) => {
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "maintenancewindows",
let: { monitorId: "$monitors._id" },
pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }],
as: "monitors.maintenanceWindows",
},
},
{
$lookup: {
from: "checks",
@@ -156,6 +164,20 @@ const getStatusPage = async (url) => {
"monitors.orderIndex": {
$indexOfArray: ["$originalMonitors", "$monitors._id"],
},
"monitors.isMaintenance": {
$reduce: {
input: "$monitors.maintenanceWindows",
initialValue: false,
in: {
$or: [
"$$value",
{
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
},
],
},
},
},
},
},
{ $match: { "monitors.orderIndex": { $ne: -1 } } },
@@ -183,7 +205,36 @@ const getStatusPage = async (url) => {
showAdminLoginLink: 1,
url: 1,
},
monitors: 1,
monitors: {
_id: 1,
userId: 1,
teamId: 1,
name: 1,
description: 1,
status: 1,
type: 1,
ignoreTlsErrors: 1,
jsonPath: 1,
expectedValue: 1,
matchMethod: 1,
url: 1,
port: 1,
isActive: 1,
interval: 1,
uptimePercentage: 1,
notifications: 1,
secret: 1,
thresholds: 1,
alertThreshold: 1,
cpuAlertThreshold: 1,
memoryAlertThreshold: 1,
diskAlertThreshold: 1,
tempAlertThreshold: 1,
checks: 1,
isMaintenance: 1,
createdAt: 1,
updatedAt: 1,
},
},
},
]);
File renamed without changes.
+55
View File
@@ -0,0 +1,55 @@
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 SettingsService from "./service/system/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
const SERVICE_NAME = "Server";
const startApp = async () => {
// 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();
// Initialize services
const services = await initializeServices(appSettings, settingsService);
// Initialize controllers
const controllers = initializeControllers(services);
const app = createApp({
services,
controllers,
appSettings,
frontendPath,
openApiSpec,
});
const port = appSettings.port || 52345;
const server = app.listen(port, () => {
logger.info({ message: `Server started on port:${port}` });
});
initShutdownListener(server, services);
};
startApp().catch((error) => {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "startApp",
stack: error.stack,
});
process.exit(1);
});
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -3,7 +3,6 @@ import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
const handleErrors = (error, req, res, next) => {
console.log("ERROR", error);
const status = error.status || 500;
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
const message = error.message || stringService.authIncorrectPassword;
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,9 +2,6 @@ import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer";
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
import Monitor from "../db/models/Monitor.js";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
@@ -33,7 +30,6 @@ class MonitorRoutes {
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
// Util routes
this.router.get("/resolution/url", isAllowed(["admin", "superadmin"]), this.monitorController.checkEndpointResolution);
this.router.get("/certificate/:monitorId", (req, res, next) => {
this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate);
});
@@ -46,7 +42,6 @@ class MonitorRoutes {
// Other static routes
this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors);
this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV);
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors);
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -3,37 +3,34 @@ const SERVICE_NAME = "checkService";
class CheckService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, stringService }) {
constructor({ db, settingsService, stringService, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.stringService = stringService;
this.errorService = errorService;
}
get serviceName() {
return CheckService.SERVICE_NAME;
}
getChecksByMonitor = async ({ monitorId, query, teamId }) => {
if (!monitorId) {
throw new Error("No monitor ID in request");
throw this.errorService.createBadRequestError("No monitor ID in request");
}
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor) {
const err = new Error("Monitor not found");
err.status = 404;
err.service = SERVICE_NAME;
err.method = "getChecksByMonitor";
throw err;
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
const err = new Error("Unauthorized");
err.status = 403;
err.service = SERVICE_NAME;
err.method = "getChecksByMonitor";
throw err;
throw this.errorService.createAuthorizationError();
}
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
@@ -55,7 +52,7 @@ class CheckService {
let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const checkData = await this.db.getChecksByTeam({
@@ -72,7 +69,7 @@ class CheckService {
getChecksSummaryByTeamId = async ({ teamId }) => {
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
@@ -81,11 +78,11 @@ class CheckService {
ackCheck = async ({ checkId, teamId, ack }) => {
if (!checkId) {
throw new Error("No check ID in request");
throw this.errorService.createBadRequestError("No check ID in request");
}
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
@@ -95,20 +92,16 @@ class CheckService {
ackAllChecks = async ({ monitorId, path, teamId, ack }) => {
if (path === "monitor") {
if (!monitorId) {
throw new Error("No monitor ID in request");
throw this.errorService.createBadRequestError("No monitor ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor) {
throw new Error("Monitor not found");
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
const err = new Error("Unauthorized");
err.status = 403;
err.service = SERVICE_NAME;
err.method = "ackAllChecks";
throw err;
throw this.errorService.createAuthorizationError();
}
}
@@ -118,29 +111,21 @@ class CheckService {
deleteChecks = async ({ monitorId, teamId }) => {
if (!monitorId) {
throw new Error("No monitor ID in request");
throw this.errorService.createBadRequestError("No monitor ID in request");
}
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor) {
const err = new Error("Monitor not found");
err.status = 404;
err.service = SERVICE_NAME;
err.method = "deleteChecks";
throw err;
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
const err = new Error("Unauthorized");
err.status = 403;
err.service = SERVICE_NAME;
err.method = "deleteChecks";
throw err;
throw this.errorService.createAuthorizationError();
}
const deletedCount = await this.db.deleteChecks(monitorId);
@@ -148,7 +133,7 @@ class CheckService {
};
deleteChecksByTeamId = async ({ teamId }) => {
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
const deletedCount = await this.db.deleteChecksByTeamId(teamId);
@@ -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
@@ -1,14 +1,18 @@
const SERVICE_NAME = "inviteService";
import { createServerError } from "../../utils/errorUtils.js";
class InviteService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, emailService, stringService }) {
constructor({ db, settingsService, emailService, stringService, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
this.stringService = stringService;
this.errorService = errorService;
}
get serviceName() {
return InviteService.SERVICE_NAME;
}
getInviteToken = async ({ invite, teamId }) => {
@@ -27,7 +31,7 @@ class InviteService {
});
const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html);
if (!result) {
throw createServerError("Failed to send invite e-mail... Please verify your settings.");
throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings.");
}
};
@@ -2,10 +2,16 @@ const SERVICE_NAME = "maintenanceWindowService";
class MaintenanceWindowService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, stringService }) {
constructor({ db, settingsService, stringService, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.stringService = stringService;
this.errorService = errorService;
}
get serviceName() {
return MaintenanceWindowService.SERVICE_NAME;
}
createMaintenanceWindow = async ({ teamId, body }) => {
@@ -15,11 +21,7 @@ class MaintenanceWindowService {
const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId));
if (unauthorizedMonitors.length > 0) {
const error = new Error("Unauthorized access to one or more monitors");
error.status = 403;
error.service = SERVICE_NAME;
error.method = "createMaintenanceWindows";
throw error;
throw this.errorService.createAuthorizationError();
}
const dbTransactions = monitorIds.map((monitorId) => {
@@ -0,0 +1,266 @@
import { createMonitorsBodyValidation } from "../../validation/joi.js";
const SERVICE_NAME = "MonitorService";
class MonitorService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService }) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
this.papaparse = papaparse;
this.logger = logger;
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)) {
throw this.errorService.createAuthorizationError();
}
};
getAllMonitors = async () => {
const monitors = await this.db.getAllMonitors();
return monitors;
};
getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const data = await this.db.getUptimeDetailsById({
monitorId,
dateRange,
normalize,
});
return data;
};
getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitorStats = await this.db.getMonitorStatsById({
monitorId,
limit,
sortOrder,
dateRange,
numToDisplay,
normalize,
});
return monitorStats;
};
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
return monitor;
};
getMonitorById = async ({ teamId, monitorId }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.getMonitorById(monitorId);
return monitor;
};
createMonitor = async ({ teamId, userId, body }) => {
const monitor = await this.db.createMonitor({
body,
teamId,
userId,
});
this.jobQueue.addJob(monitor._id, monitor);
};
createBulkMonitors = async ({ fileData, userId, teamId }) => {
const { parse } = this.papaparse;
return new Promise((resolve, reject) => {
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 this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
}
return num;
}
return value;
},
complete: async ({ data, errors }) => {
try {
if (errors.length > 0) {
throw this.errorService.createServerError("Error parsing CSV");
}
if (!data || data.length === 0) {
throw this.errorService.createServerError("CSV file contains no data rows");
}
const enrichedData = data.map((monitor) => ({
userId,
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) => {
this.jobQueue.addJob(monitor._id, monitor);
})
);
resolve(monitors);
} catch (error) {
reject(error);
}
},
});
});
};
deleteMonitor = async ({ teamId, monitorId }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.deleteMonitor({ teamId, monitorId });
await this.jobQueue.deleteJob(monitor);
await this.db.deleteStatusPagesByMonitorId(monitor._id);
return monitor;
};
deleteAllMonitors = async ({ 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) {
this.logger.warn({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
stack: error.stack,
});
}
})
);
return deletedCount;
};
editMonitor = async ({ teamId, monitorId, body }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const editedMonitor = await this.db.editMonitor({ monitorId, body });
await this.jobQueue.updateJob(editedMonitor);
};
pauseMonitor = async ({ teamId, monitorId }) => {
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 monitor;
};
addDemoMonitors = async ({ userId, teamId }) => {
const demoMonitors = await this.db.addDemoMonitors(userId, teamId);
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
return demoMonitors;
};
sendTestEmail = async ({ to }) => {
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 this.errorService.createServerError("Failed to send test email.");
}
return messageId;
};
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
const monitors = await this.db.getMonitorsByTeamId({
limit,
type,
page,
rowsPerPage,
filter,
field,
order,
teamId,
});
return monitors;
};
getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => {
const result = await this.db.getMonitorsAndSummaryByTeamId({
type,
explain,
teamId,
});
return result;
};
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
const result = await this.db.getMonitorsWithChecksByTeamId({
limit,
type,
page,
rowsPerPage,
filter,
field,
order,
teamId,
explain,
});
return result;
};
exportMonitorsToCSV = async ({ teamId }) => {
const monitors = await this.db.getMonitorsByTeamId({ teamId });
if (!monitors || monitors.length === 0) {
throw this.errorService.createNotFoundError("No monitors to export");
}
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 = this.papaparse.unparse(csvData);
return csv;
};
}
export default MonitorService;
@@ -1,15 +1,22 @@
const SERVICE_NAME = "userService";
import { createAuthError, createError } from "../../utils/errorUtils.js";
class UserService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, emailService, settingsService, logger, stringService, jwt }) {
constructor({ crypto, db, emailService, settingsService, logger, stringService, jwt, errorService, jobQueue }) {
this.db = db;
this.emailService = emailService;
this.settingsService = settingsService;
this.logger = logger;
this.stringService = stringService;
this.jwt = jwt;
this.errorService = errorService;
this.jobQueue = jobQueue;
this.crypto = crypto;
}
get serviceName() {
return UserService.SERVICE_NAME;
}
issueToken = (payload, appSettings) => {
@@ -80,7 +87,7 @@ class UserService {
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
throw createAuthError(this.stringService.authIncorrectPassword);
throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword);
}
// Remove password from user object. Should this be abstracted to DB layer?
@@ -109,7 +116,7 @@ class UserService {
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
throw createError(this.stringService.authIncorrectPassword, 403);
throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword);
}
// If a match, update the password
updates.password = updates.newPassword;
@@ -154,23 +161,23 @@ class UserService {
deleteUser = async (user) => {
const email = user?.email;
if (!email) {
throw new Error("No email in request");
throw this.errorService.createBadRequestError("No email in request");
}
const teamId = user?.teamId;
const userId = user?._id;
if (!teamId) {
throw new Error("No team ID in request");
throw this.errorService.createBadRequestError("No team ID in request");
}
if (!userId) {
throw new Error("No user ID in request");
throw this.errorService.createBadRequestError("No user ID in request");
}
const roles = user?.role;
if (roles.includes("demo")) {
throw new Error("Demo user cannot be deleted");
throw this.errorService.createBadRequestError("Demo user cannot be deleted");
}
// 1. Find all the monitors associated with the team ID if superadmin
@@ -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) => {
@@ -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();
@@ -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, {
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more