From 066df2c64f7332a331e0b0b89bf7dabf2a871037 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Fri, 18 Jul 2025 10:59:02 +0100 Subject: [PATCH 01/11] Relax monitor URL validation --- client/src/Validation/validation.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index b2fe7de71..7459de13d 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -128,26 +128,21 @@ const monitorValidation = joi.object({ .string() .trim() .custom((value, helpers) => { - // Regex from https://gist.github.com/dperini/729294 + const noProtocol = value.replace(/^(https?:\/\/)/, ""); + + if (/^[a-zA-Z0-9-]+(:\d{1,5})?$/.test(noProtocol)) { + return value; + } + + // Accept full domain/IP/URL (as fallback) var urlRegex = new RegExp( "^" + - // protocol identifier (optional) - // short syntax // still required "(?:(?:https?|ftp):\\/\\/)?" + - // user:pass BasicAuth (optional) "(?:" + - // IP address dotted notation octets - // excludes loopback network 0.0.0.0 - // excludes reserved space >= 224.0.0.0 - // excludes network & broadcast addresses - // (first & last IP address of each class) "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + "|" + - // host & domain names, may end with dot - // can be replaced by a shortest alternative - // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ "(?:" + "(?:" + "[a-z0-9\\u00a1-\\uffff]" + @@ -155,21 +150,19 @@ const monitorValidation = joi.object({ ")?" + "[a-z0-9\\u00a1-\\uffff]\\." + ")+" + - // TLD identifier name, may end with dot "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + ")" + - // port number (optional) "(?::\\d{2,5})?" + - // resource path (optional) "(?:[/?#]\\S*)?" + "$", "i" ); - if (!urlRegex.test(value)) { - return helpers.error("string.invalidUrl"); + + if (urlRegex.test(value)) { + return value; } - return value; + return helpers.error("string.invalidUrl"); }) .messages({ "string.empty": "This field is required.", From b823f0f7db0f0cee6bd0c511968883a2027fae92 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Fri, 18 Jul 2025 11:40:20 +0100 Subject: [PATCH 02/11] Relax monitor URL validation for Docker and local hostnames (fixes #2605) --- client/src/Validation/validation.js | 2 +- server/validation/joi.js | 47 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 7459de13d..887e4e487 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -130,7 +130,7 @@ const monitorValidation = joi.object({ .custom((value, helpers) => { const noProtocol = value.replace(/^(https?:\/\/)/, ""); - if (/^[a-zA-Z0-9-]+(:\d{1,5})?$/.test(noProtocol)) { + if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { return value; } diff --git a/server/validation/joi.js b/server/validation/joi.js index f555756eb..8549d470a 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -168,7 +168,52 @@ const createMonitorBodyValidation = joi.object({ name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), - url: joi.string().required(), + url: joi + .string() + .required() + .custom((value, helpers) => { + const noProtocol = value.replace(/^(https?:\/\/)/, ""); + + if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { + return value; + } + + // Existing complex URL validation as fallback + var urlRegex = new RegExp( + "^" + + "(?:(?:https?|ftp):\\/\\/)?" + + "(?:" + + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + + "|" + + "(?:" + + "(?:" + + "[a-z0-9\\u00a1-\\uffff]" + + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + + ")?" + + "[a-z0-9\\u00a1-\\uffff]\\." + + ")+" + + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + + ")" + + "(?::\\d{2,5})?" + + "(?:[/?#]\\S*)?" + + "$", + "i" + ); + + if (urlRegex.test(value)) { + return value; + } + + return helpers.error("string.invalidUrl"); + }) + .messages({ + "string.empty": "This field is required.", + "string.uri": "The URL you provided is not valid.", + "string.invalidUrl": + "Please enter a valid URL, hostname, or container name (with optional port).", + }), ignoreTlsErrors: joi.boolean().default(false), port: joi.number(), isActive: joi.boolean(), From 8ffd9a5e2a396a10a9ba9b136efaa198c81d63b4 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Fri, 18 Jul 2025 11:58:27 +0100 Subject: [PATCH 03/11] Tighten Docker hostname regex for monitor URL validation --- client/src/Validation/validation.js | 2 +- server/validation/joi.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 887e4e487..7676d65d8 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -130,7 +130,7 @@ const monitorValidation = joi.object({ .custom((value, helpers) => { const noProtocol = value.replace(/^(https?:\/\/)/, ""); - if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { return value; } diff --git a/server/validation/joi.js b/server/validation/joi.js index 8549d470a..3fb652736 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -174,7 +174,7 @@ const createMonitorBodyValidation = joi.object({ .custom((value, helpers) => { const noProtocol = value.replace(/^(https?:\/\/)/, ""); - if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { return value; } From 945cff978bd9c54cec974096f310230667684c72 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Sat, 19 Jul 2025 06:53:01 +0100 Subject: [PATCH 04/11] Use URL.canParse for URL validation; add strict regex fallback (maintainer feedback) --- client/src/Validation/validation.js | 56 ++++++++++++++--------------- server/validation/joi.js | 56 ++++++++++++++--------------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 7676d65d8..94575ce99 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -128,40 +128,36 @@ const monitorValidation = joi.object({ .string() .trim() .custom((value, helpers) => { - const noProtocol = value.replace(/^(https?:\/\/)/, ""); + // 1. Standard URLs: must have protocol and pass canParse() + if (/^(https?:\/\/)/.test(value)) { + if ( + typeof URL !== "undefined" && + typeof URL.canParse === "function" && + URL.canParse(value) + ) { + return value; + } + // else, it's a malformed URL with protocol + return helpers.error("string.invalidUrl"); + } - if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { - return value; - } - - // Accept full domain/IP/URL (as fallback) - var urlRegex = new RegExp( - "^" + - "(?:(?:https?|ftp):\\/\\/)?" + - "(?:" + - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + - "|" + - "(?:" + - "(?:" + - "[a-z0-9\\u00a1-\\uffff]" + - "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + - ")?" + - "[a-z0-9\\u00a1-\\uffff]\\." + - ")+" + - "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + - ")" + - "(?::\\d{2,5})?" + - "(?:[/?#]\\S*)?" + - "$", - "i" - ); - - if (urlRegex.test(value)) { + // 2. Docker/internal hostnames (no protocol) + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) { + if (value.includes("..")) { + return helpers.error("string.invalidUrl"); + } + // Split by dot, check each label + const labels = value.split(":")[0].split("."); + for (const label of labels) { + if (!label) return helpers.error("string.invalidUrl"); + if (label.startsWith("-") || label.endsWith("-")) { + return helpers.error("string.invalidUrl"); + } + } return value; } + // 3. Everything else is invalid return helpers.error("string.invalidUrl"); }) .messages({ diff --git a/server/validation/joi.js b/server/validation/joi.js index 3fb652736..bf69bd06f 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -172,40 +172,36 @@ const createMonitorBodyValidation = joi.object({ .string() .required() .custom((value, helpers) => { - const noProtocol = value.replace(/^(https?:\/\/)/, ""); + // 1. Standard URLs: must have protocol and pass canParse() + if (/^(https?:\/\/)/.test(value)) { + if ( + typeof URL !== "undefined" && + typeof URL.canParse === "function" && + URL.canParse(value) + ) { + return value; + } + // else, it's a malformed URL with protocol + return helpers.error("string.invalidUrl"); + } - if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { - return value; - } - - // Existing complex URL validation as fallback - var urlRegex = new RegExp( - "^" + - "(?:(?:https?|ftp):\\/\\/)?" + - "(?:" + - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + - "|" + - "(?:" + - "(?:" + - "[a-z0-9\\u00a1-\\uffff]" + - "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + - ")?" + - "[a-z0-9\\u00a1-\\uffff]\\." + - ")+" + - "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + - ")" + - "(?::\\d{2,5})?" + - "(?:[/?#]\\S*)?" + - "$", - "i" - ); - - if (urlRegex.test(value)) { + // 2. Docker/internal hostnames (no protocol) + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) { + if (value.includes("..")) { + return helpers.error("string.invalidUrl"); + } + // Split by dot, check each label + const labels = value.split(":")[0].split("."); + for (const label of labels) { + if (!label) return helpers.error("string.invalidUrl"); + if (label.startsWith("-") || label.endsWith("-")) { + return helpers.error("string.invalidUrl"); + } + } return value; } + // 3. Everything else is invalid return helpers.error("string.invalidUrl"); }) .messages({ From 4921b17f11eab7d9d666b4321187a54f265d9a3e Mon Sep 17 00:00:00 2001 From: Miguel Rodriguez Date: Sun, 20 Jul 2025 12:57:16 -0400 Subject: [PATCH 05/11] fix: docker.sock doesn't exist on many K8S Servers --- charts/helm/checkmate/templates/server-deployment.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml index 8548495dd..6f21f86f3 100644 --- a/charts/helm/checkmate/templates/server-deployment.yaml +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -20,11 +20,3 @@ spec: envFrom: - secretRef: name: checkmate-secrets - volumeMounts: - - name: docker-sock - mountPath: /var/run/docker.sock - volumes: - - name: docker-sock - hostPath: - path: /var/run/docker.sock - type: Socket From 37655a64c0ccd962b0f0529ad43998c3a697efd2 Mon Sep 17 00:00:00 2001 From: Miguel Rodriguez Date: Sun, 20 Jul 2025 13:22:01 -0400 Subject: [PATCH 06/11] fix: Set CORS headers with a split setup on helm. --- charts/helm/checkmate/templates/server-ingress.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml index acf64f95f..06f3a4edd 100644 --- a/charts/helm/checkmate/templates/server-ingress.yaml +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -5,6 +5,11 @@ metadata: name: server-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }}" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" spec: rules: - host: {{ .Values.server.ingress.host }} From 2d437e2016bef77a04fe894319fa2070899d34f3 Mon Sep 17 00:00:00 2001 From: Miguel Rodriguez Date: Sun, 20 Jul 2025 13:30:01 -0400 Subject: [PATCH 07/11] coderabbit fix: Add HTTPS to CORS origins --- charts/helm/checkmate/templates/server-ingress.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml index 06f3a4edd..c1982d451 100644 --- a/charts/helm/checkmate/templates/server-ingress.yaml +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -6,7 +6,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/enable-cors: "true" - nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }}" + nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}" nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" nginx.ingress.kubernetes.io/cors-allow-credentials: "true" From f2037bf5fe56b0c205bd51ed37517e8ee60611b8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Jul 2025 13:46:06 -0700 Subject: [PATCH 08/11] fix route --- client/src/Utils/NetworkService.js | 2 +- server/routes/monitorRoute.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 54bb239a4..f1258091b 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -977,7 +977,7 @@ class NetworkService { }); } - return this.axiosInstance.get(`/monitors/summary/team?${params.toString()}`, { + return this.axiosInstance.get(`/monitors/team/summary?${params.toString()}`, { headers: { "Content-Type": "application/json", }, diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js index 2911bf9d7..3f4a40c1a 100755 --- a/server/routes/monitorRoute.js +++ b/server/routes/monitorRoute.js @@ -20,7 +20,7 @@ class MonitorRoutes { // Team routes this.router.get("/team", this.monitorController.getMonitorsByTeamId); this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/summary/team", this.monitorController.getMonitorsAndSummaryByTeamId); // TODO should be /team/summary + this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); // TODO should be /team/summary // Uptime routes this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats); From fdff3be316d8ce3b2e3fb951c7ba54d54499afa9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Jul 2025 13:58:01 -0700 Subject: [PATCH 09/11] fix team auth --- server/controllers/monitorController.js | 94 +++++++++++++++++------- server/db/mongo/modules/monitorModule.js | 48 ------------ server/routes/monitorRoute.js | 12 +-- 3 files changed, 71 insertions(+), 83 deletions(-) diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js index 79579e521..9943a765a 100755 --- a/server/controllers/monitorController.js +++ b/server/controllers/monitorController.js @@ -20,6 +20,7 @@ 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 { @@ -31,6 +32,15 @@ class MonitorController { 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 @@ -52,32 +62,18 @@ class MonitorController { "getAllMonitors" ); - /** - * Returns all monitors with uptime stats for 1,7,30, and 90 days - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @param {function} next - * @returns {Promise} - * @throws {Error} - */ - getAllMonitorsWithUptimeStats = asyncHandler( - async (req, res, next) => { - const monitors = await this.db.getAllMonitorsWithUptimeStats(); - return res.success({ - msg: this.stringService.monitorGetAll, - data: monitors, - }); - }, - SERVICE_NAME, - "getAllMonitorsWithUptimeStats" - ); - 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, @@ -109,6 +105,13 @@ class MonitorController { 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, @@ -140,8 +143,14 @@ class MonitorController { await getHardwareDetailsByIdParamValidation.validateAsync(req.params); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); - const { monitorId } = req.params; - const { dateRange } = 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, @@ -153,7 +162,7 @@ class MonitorController { ); getMonitorCertificate = asyncHandler( - async (req, res, next, fetchMonitorCertificate) => { + async (req, res, next) => { await getCertificateParamValidation.validateAsync(req.params); const { monitorId } = req.params; @@ -188,6 +197,18 @@ class MonitorController { 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, @@ -367,6 +388,13 @@ class MonitorController { 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); @@ -389,7 +417,7 @@ class MonitorController { */ deleteAllMonitors = asyncHandler( async (req, res, next) => { - const { teamId } = req.user; + const teamId = req?.user?.teamId; const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId); await Promise.all( monitors.map(async (monitor) => { @@ -431,7 +459,14 @@ class MonitorController { async (req, res, next) => { await getMonitorByIdParamValidation.validateAsync(req.params); await editMonitorBodyValidation.validateAsync(req.body); - const { monitorId } = 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 editedMonitor = await this.db.editMonitor(monitorId, req.body); @@ -462,6 +497,13 @@ class MonitorController { 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); diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js index b331bc337..3192b711c 100755 --- a/server/db/mongo/modules/monitorModule.js +++ b/server/db/mongo/modules/monitorModule.js @@ -56,53 +56,6 @@ const getAllMonitors = async (req, res) => { } }; -/** - * Get all monitors with uptime stats for 1,7,30, and 90 days - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise>} - * @throws {Error} - */ -const getAllMonitorsWithUptimeStats = async () => { - const timeRanges = { - 1: new Date(Date.now() - 24 * 60 * 60 * 1000), - 7: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - 30: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), - 90: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), - }; - - try { - const monitors = await Monitor.find(); - const monitorsWithStats = await Promise.all( - monitors.map(async (monitor) => { - const model = CHECK_MODEL_LOOKUP[monitor.type]; - - const uptimeStats = await Promise.all( - Object.entries(timeRanges).map(async ([days, startDate]) => { - const checks = await model.find({ - monitorId: monitor._id, - createdAt: { $gte: startDate }, - }); - return [days, getUptimePercentage(checks)]; - }) - ); - - return { - ...monitor.toObject(), - ...Object.fromEntries(uptimeStats), - }; - }) - ); - - return monitorsWithStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitorsWithUptimeStats"; - throw error; - } -}; - /** * Function to calculate uptime duration based on the most recent check. * @param {Array} checks Array of check objects. @@ -761,7 +714,6 @@ const pauseMonitor = async ({ monitorId }) => { export { getAllMonitors, - getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, getMonitorsByIds, diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js index 3f4a40c1a..15553fc1c 100755 --- a/server/routes/monitorRoute.js +++ b/server/routes/monitorRoute.js @@ -20,10 +20,9 @@ class MonitorRoutes { // Team routes this.router.get("/team", this.monitorController.getMonitorsByTeamId); this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); // TODO should be /team/summary + this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); // Uptime routes - this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats); this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById); // Hardware routes @@ -53,13 +52,8 @@ class MonitorRoutes { // Individual monitor CRUD routes this.router.get("/:monitorId", this.monitorController.getMonitorById); - this.router.put("/:monitorId", verifyTeamAccess(Monitor, "monitorId"), isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor); - this.router.delete( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - isAllowed(["admin", "superadmin"]), - this.monitorController.deleteMonitor - ); + this.router.put("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor); + this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor); } getRouter() { From 1b085236c7ae6165cf926f0911d1d45fe48fa836 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Jul 2025 14:11:09 -0700 Subject: [PATCH 10/11] fix aith for notification controller --- server/controllers/notificationController.js | 76 +++++++++++++++++-- server/db/mongo/modules/notificationModule.js | 5 ++ server/routes/notificationRoute.js | 10 +-- 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index 4e21e7284..41af21ece 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -39,9 +39,19 @@ class NotificationController { }); const body = req.body; - const { _id, teamId } = req.user; - body.userId = _id; + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + const userId = req?.user?._id; + if (!userId) { + throw new Error("No user ID in request"); + } + body.userId = userId; body.teamId = teamId; + const notification = await this.db.createNotification(body); return res.success({ msg: "Notification created successfully", @@ -54,7 +64,13 @@ class NotificationController { getNotificationsByTeamId = asyncHandler( async (req, res, next) => { - const notifications = await this.db.getNotificationsByTeamId(req.user.teamId); + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + const notifications = await this.db.getNotificationsByTeamId(teamId); + return res.success({ msg: "Notifications fetched successfully", data: notifications, @@ -66,6 +82,18 @@ class NotificationController { deleteNotification = asyncHandler( async (req, res, next) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + const notification = await this.db.getNotificationById(req.params.id); + if (!notification.teamId.equals(teamId)) { + const error = new Error("Unauthorized"); + error.status = 403; + throw error; + } + await this.db.deleteNotificationById(req.params.id); return res.success({ msg: "Notification deleted successfully", @@ -78,6 +106,17 @@ class NotificationController { getNotificationById = asyncHandler( async (req, res, next) => { const notification = await this.db.getNotificationById(req.params.id); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + if (!notification.teamId.equals(teamId)) { + const error = new Error("Unauthorized"); + error.status = 403; + throw error; + } return res.success({ msg: "Notification fetched successfully", data: notification, @@ -93,10 +132,23 @@ class NotificationController { abortEarly: false, }); - const notification = await this.db.editNotification(req.params.id, req.body); + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + const notification = await this.db.getNotificationById(req.params.id); + + if (!notification.teamId.equals(teamId)) { + const error = new Error("Unauthorized"); + error.status = 403; + throw error; + } + + const editedNotification = await this.db.editNotification(req.params.id, req.body); return res.success({ msg: "Notification updated successfully", - data: notification, + data: editedNotification, }); }, SERVICE_NAME, @@ -105,8 +157,20 @@ class NotificationController { testAllNotifications = asyncHandler( async (req, res, next) => { - const { monitorId } = req.body; + const monitorId = req.body.monitorId; + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + const monitor = await this.db.getMonitorById(monitorId); + + if (!monitor.teamId.equals(teamId)) { + const error = new Error("Unauthorized"); + error.status = 403; + throw error; + } + const notifications = monitor.notifications; if (notifications.length === 0) throw new Error("No notifications"); const result = await this.notificationService.testAllNotifications(notifications); diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js index 08d57b1a9..2348a6321 100755 --- a/server/db/mongo/modules/notificationModule.js +++ b/server/db/mongo/modules/notificationModule.js @@ -74,6 +74,11 @@ const deleteNotificationsByMonitorId = async (monitorId) => { const deleteNotificationById = async (id) => { try { + const notification = await Notification.findById(id); + if (!notification) { + throw new Error("Notification not found"); + } + const result = await Notification.findByIdAndDelete(id); await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); return result; diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js index 6c77d8519..f88b0b96c 100755 --- a/server/routes/notificationRoute.js +++ b/server/routes/notificationRoute.js @@ -1,8 +1,4 @@ import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js"; -import Notification from "../db/models/Notification.js"; class NotificationRoutes { constructor(notificationController) { this.router = Router(); @@ -11,8 +7,6 @@ class NotificationRoutes { } initializeRoutes() { - this.router.use(verifyJWT); - this.router.post("/", this.notificationController.createNotification); this.router.post("/test/all", this.notificationController.testAllNotifications); @@ -20,9 +14,9 @@ class NotificationRoutes { this.router.get("/team", this.notificationController.getNotificationsByTeamId); - this.router.delete("/:id", verifyOwnership(Notification, "id"), this.notificationController.deleteNotification); this.router.get("/:id", this.notificationController.getNotificationById); - this.router.put("/:id", verifyTeamAccess(Notification, "id"), this.notificationController.editNotification); + this.router.delete("/:id", this.notificationController.deleteNotification); + this.router.put("/:id", this.notificationController.editNotification); } getRouter() { From 48848d1d43eeaceb12aa93004d9efe3fad87d46f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Jul 2025 15:05:17 -0700 Subject: [PATCH 11/11] prevent demo user from being deleted --- server/controllers/authController.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 1cc1a747a..ace63b662 100755 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -340,7 +340,6 @@ class AuthController { if (!email) { throw new Error("No email in request"); } - // 1. Find all the monitors associated with the team ID if superadmin const teamId = req?.user?.teamId; const userId = req?.user?._id; @@ -354,7 +353,11 @@ class AuthController { } const roles = req.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, });