From 9e067589971a035e8f7bae86a9f493d2d01c0d75 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Tue, 3 Mar 2026 23:28:11 +0530 Subject: [PATCH 01/15] added a PATCH/notifications/bulk/monitor --- client/package-lock.json | 22 +---- server/package-lock.json | 81 ------------------- .../src/controllers/notificationController.ts | 35 ++++++++ .../monitors/IMonitorsRepository.ts | 1 + .../monitors/MongoMonitorsRepository.ts | 26 ++++++ server/src/routes/notificationRoute.ts | 2 + 6 files changed, 66 insertions(+), 101 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 038814543..a8b5bcdcc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -80,7 +80,6 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -351,7 +350,6 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -389,7 +387,6 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1050,7 +1047,6 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1155,7 +1151,6 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2179,7 +2174,6 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2195,7 +2189,6 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2248,7 +2241,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2553,7 +2545,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2966,8 +2957,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3343,7 +3333,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4049,7 +4038,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5354,7 +5342,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5366,7 +5353,6 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5542,8 +5528,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -5682,7 +5667,6 @@ "node_modules/rollup": { "version": "4.57.1", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6363,7 +6347,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6467,7 +6450,6 @@ "node_modules/vite": { "version": "6.3.6", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/server/package-lock.json b/server/package-lock.json index d89613ecf..517c58efd 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -874,7 +874,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1504,7 +1503,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1549,7 +1547,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -4223,16 +4220,6 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -4405,7 +4392,6 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4566,7 +4552,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5053,7 +5038,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5632,7 +5616,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6575,7 +6558,6 @@ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", "license": "MIT", - "peer": true, "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" @@ -7330,7 +7312,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7692,7 +7673,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9341,7 +9321,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -12231,7 +12210,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14173,63 +14151,6 @@ "node": ">=8" } }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/svgo/node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/svgo/node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0", - "optional": true - }, "node_modules/swagger-ui-dist": { "version": "5.30.0", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", @@ -14513,7 +14434,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14676,7 +14596,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index f32f80ddc..78f134fb6 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -154,6 +154,41 @@ class NotificationController { } }; + bulkEditMonitorNotifications = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = requireTeamId(req.user?.teamId); + + const { monitorIds, notificationIds, action } = req.body; + + if (!monitorIds || !Array.isArray(monitorIds) || monitorIds.length === 0) { + throw new AppError({ message: "Monitor IDs are required", status: 400 }); + } + + if (!notificationIds || !Array.isArray(notificationIds) || notificationIds.length === 0) { + throw new AppError({ message: "Notification IDs are required", status: 400 }); + } + + if (!action || !["add", "remove", "set"].includes(action)) { + throw new AppError({ message: "Action must be 'add', 'remove', or 'set'", status: 400 }); + } + + const modifiedCount = await this.monitorsRepository.bulkUpdateNotifications( + monitorIds, + notificationIds, + action, + teamId + ); + + return res.status(200).json({ + success: true, + msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`, + data: { modifiedCount }, + }); + } catch (error) { + next(error); + } + }; + testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { try { const monitorId = req.body.monitorId; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 9d82d9987..0732f93dd 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -41,6 +41,7 @@ export interface IMonitorsRepository { findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; removeNotificationFromMonitors(notificationId: string): Promise; + bulkUpdateNotifications(monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set", teamId: string): Promise; deleteByTeamIdsNotIn(teamIds: string[]): Promise; findAllMonitorIds(): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 4ae4394d4..53d686554 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -298,6 +298,32 @@ class MongoMonitorsRepository implements IMonitorsRepository { await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); }; + bulkUpdateNotifications = async ( + monitorIds: string[], + notificationIds: string[], + action: "add" | "remove" | "set", + teamId: string + ): Promise => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) }; + + let update; + switch (action) { + case "add": + update = { $addToSet: { notifications: { $each: notificationIds } } }; + break; + case "remove": + update = { $pull: { notifications: { $in: notificationIds } } }; + break; + case "set": + update = { $set: { notifications: notificationIds } }; + break; + } + + const result = await MonitorModel.updateMany(filter, update); + return result.modifiedCount; + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/routes/notificationRoute.ts b/server/src/routes/notificationRoute.ts index a1802d3b9..7a4b73df9 100755 --- a/server/src/routes/notificationRoute.ts +++ b/server/src/routes/notificationRoute.ts @@ -17,6 +17,8 @@ class NotificationRoutes { this.router.get("/team", this.notificationController.getNotificationsByTeamId); + this.router.patch("/bulk/monitors", this.notificationController.bulkEditMonitorNotifications); + this.router.get("/:id", this.notificationController.getNotificationById); this.router.delete("/:id", this.notificationController.deleteNotification); this.router.patch("/:id", this.notificationController.editNotification); From 4827f8faecbb1ac96c1de7032a7c1b73be62cdf4 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Thu, 5 Mar 2026 13:06:36 +0530 Subject: [PATCH 02/15] refractor: simplify bulk edit route to PATCH /notifications --- server/src/routes/notificationRoute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/notificationRoute.ts b/server/src/routes/notificationRoute.ts index 7a4b73df9..9961000d3 100755 --- a/server/src/routes/notificationRoute.ts +++ b/server/src/routes/notificationRoute.ts @@ -17,7 +17,7 @@ class NotificationRoutes { this.router.get("/team", this.notificationController.getNotificationsByTeamId); - this.router.patch("/bulk/monitors", this.notificationController.bulkEditMonitorNotifications); + this.router.patch("/", this.notificationController.bulkEditMonitorNotifications); this.router.get("/:id", this.notificationController.getNotificationById); this.router.delete("/:id", this.notificationController.deleteNotification); From eb2d8b8d3ba63eec30fffdd280f1e48d89ac1249 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Thu, 5 Mar 2026 13:58:53 +0530 Subject: [PATCH 03/15] style: format notificationController with prettier --- server/src/controllers/notificationController.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 78f134fb6..fe8f552a9 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -172,12 +172,7 @@ class NotificationController { throw new AppError({ message: "Action must be 'add', 'remove', or 'set'", status: 400 }); } - const modifiedCount = await this.monitorsRepository.bulkUpdateNotifications( - monitorIds, - notificationIds, - action, - teamId - ); + const modifiedCount = await this.monitorsRepository.bulkUpdateNotifications(monitorIds, notificationIds, action, teamId); return res.status(200).json({ success: true, From 8eca81c8bef656e8be7575dac121d3b16a6f082b Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Thu, 5 Mar 2026 18:02:55 +0530 Subject: [PATCH 04/15] fix: handle malformed IDs and allow empty set action --- server/src/controllers/notificationController.ts | 10 ++++++++-- .../repositories/monitors/MongoMonitorsRepository.ts | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index fe8f552a9..403375508 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -164,14 +164,20 @@ class NotificationController { throw new AppError({ message: "Monitor IDs are required", status: 400 }); } - if (!notificationIds || !Array.isArray(notificationIds) || notificationIds.length === 0) { - throw new AppError({ message: "Notification IDs are required", status: 400 }); + if (!notificationIds || !Array.isArray(notificationIds)) { + throw new AppError({ message: "Notification IDs must be an array", status: 400 }); } if (!action || !["add", "remove", "set"].includes(action)) { throw new AppError({ message: "Action must be 'add', 'remove', or 'set'", status: 400 }); } + // "add" and "remove" need at least one notification ID + // "set" can be empty (to clear all notifications from monitors) + if (notificationIds.length === 0 && action !== "set") { + throw new AppError({ message: "Notification IDs are required for add/remove actions", status: 400 }); + } + const modifiedCount = await this.monitorsRepository.bulkUpdateNotifications(monitorIds, notificationIds, action, teamId); return res.status(200).json({ diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 53d686554..529ce2413 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -304,7 +304,12 @@ class MongoMonitorsRepository implements IMonitorsRepository { action: "add" | "remove" | "set", teamId: string ): Promise => { - const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + let objectIds; + try { + objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + } catch { + throw new AppError({ message: "One or more monitor IDs are invalid", status: 400 }); + } const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) }; let update; From 9c17fbe79c7d3d5eb3e1d7b37b2627509f9cd3f6 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Thu, 5 Mar 2026 23:06:41 +0530 Subject: [PATCH 05/15] refactor: add Joi validation, rename to updateNotifications, add default case --- .../src/controllers/notificationController.ts | 29 +++++-------------- .../monitors/IMonitorsRepository.ts | 2 +- .../monitors/MongoMonitorsRepository.ts | 4 ++- server/src/validation/joi.ts | 15 ++++++++++ 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 403375508..c913a65fb 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { Notification } from "@/types/index.js"; -import { createNotificationBodyValidation } from "@/validation/joi.js"; +import { createNotificationBodyValidation, bulkEditNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; @@ -154,31 +154,18 @@ class NotificationController { } }; + // Handler for bulk editing monitor notification associations bulkEditMonitorNotifications = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req.user?.teamId); + // Joi validates monitorIds, notificationIds, and action automatically + await bulkEditNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + const teamId = requireTeamId(req.user?.teamId); const { monitorIds, notificationIds, action } = req.body; - if (!monitorIds || !Array.isArray(monitorIds) || monitorIds.length === 0) { - throw new AppError({ message: "Monitor IDs are required", status: 400 }); - } - - if (!notificationIds || !Array.isArray(notificationIds)) { - throw new AppError({ message: "Notification IDs must be an array", status: 400 }); - } - - if (!action || !["add", "remove", "set"].includes(action)) { - throw new AppError({ message: "Action must be 'add', 'remove', or 'set'", status: 400 }); - } - - // "add" and "remove" need at least one notification ID - // "set" can be empty (to clear all notifications from monitors) - if (notificationIds.length === 0 && action !== "set") { - throw new AppError({ message: "Notification IDs are required for add/remove actions", status: 400 }); - } - - const modifiedCount = await this.monitorsRepository.bulkUpdateNotifications(monitorIds, notificationIds, action, teamId); + const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId); return res.status(200).json({ success: true, diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 0732f93dd..88c039881 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -41,7 +41,7 @@ export interface IMonitorsRepository { findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; removeNotificationFromMonitors(notificationId: string): Promise; - bulkUpdateNotifications(monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set", teamId: string): Promise; + updateNotifications(monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set", teamId: string): Promise; deleteByTeamIdsNotIn(teamIds: string[]): Promise; findAllMonitorIds(): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 529ce2413..63e38721c 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -298,7 +298,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); }; - bulkUpdateNotifications = async ( + updateNotifications = async ( monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set", @@ -323,6 +323,8 @@ class MongoMonitorsRepository implements IMonitorsRepository { case "set": update = { $set: { notifications: notificationIds } }; break; + default: + throw new AppError({ message: `Invalid action: ${action}`, status: 400 }); } const result = await MonitorModel.updateMany(filter, update); diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 912767587..cfe2502a6 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -573,6 +573,20 @@ const triggerNotificationBodyValidation = joi.object({ }), }); +const bulkEditNotificationBodyValidation = joi.object({ + monitorIds: joi.array().items(joi.string()).min(1).required().messages({ + "array.min": "At least one monitor ID is required", + "any.required": "Monitor IDs are required", + }), + notificationIds: joi.array().items(joi.string()).required().messages({ + "any.required": "Notification IDs are required", + }), + action: joi.string().valid("add", "remove", "set").required().messages({ + "any.only": "Action must be 'add', 'remove', or 'set'", + "any.required": "Action is required", + }), +}); + const createNotificationBodyValidation = joi.object({ notificationName: joi.string().required().messages({ "string.empty": "Notification name is required", @@ -793,6 +807,7 @@ export { imageValidation, triggerNotificationBodyValidation, createNotificationBodyValidation, + bulkEditNotificationBodyValidation, webhookConfigValidation, createAnnouncementValidation, sendTestEmailBodyValidation, From a40a76fb3ca30bd6698736fdd341365bed7cc72b Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Fri, 6 Mar 2026 13:22:32 +0530 Subject: [PATCH 06/15] fix: resolve Zod build error and run formatter --- server/package-lock.json | 1 - server/src/validation/notificationValidation.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 86002bba5..5a8c1ed93 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -14637,7 +14637,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index 7b02c7022..3e2f16de4 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -82,6 +82,6 @@ export const bulkEditNotificationBodyValidation = z.object({ monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), notificationIds: z.array(z.string()), action: z.enum(["add", "remove", "set"], { - errorMap: () => ({message: "Action must be 'add', 'remove', or 'set'"}) + message: "Action must be 'add', 'remove', or 'set'", }), -}); \ No newline at end of file +}); From c5f0316990e03cec8c62c95071eef8eb589b892f Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Fri, 6 Mar 2026 20:41:33 +0530 Subject: [PATCH 07/15] fix(validation): remove invalid zod enum argument --- server/src/validation/notificationValidation.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index 3e2f16de4..e5b84f24e 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -81,7 +81,5 @@ export const sendTestEmailBodyValidation = z.object({ export const bulkEditNotificationBodyValidation = z.object({ monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), notificationIds: z.array(z.string()), - action: z.enum(["add", "remove", "set"], { - message: "Action must be 'add', 'remove', or 'set'", - }), + action: z.enum(["add", "remove", "set"]), }); From 27dd565bc6e635eeb481e54444a06398bdeeb698 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Fri, 6 Mar 2026 21:07:29 +0530 Subject: [PATCH 08/15] feat(ui): add bulk edit notifications modal and checkmark selection for uptime monitors --- client/package-lock.json | 100 +++++-------- client/package.json | 1 + .../Components/BulkEditNotificationsModal.tsx | 134 ++++++++++++++++++ .../Components/UptimeMonitorsTable.tsx | 36 +++++ client/src/Pages/Uptime/Monitors/index.tsx | 73 +++++++--- 5 files changed, 263 insertions(+), 81 deletions(-) create mode 100644 client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 338de1e26..aaba3b828 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,6 +18,7 @@ "@mui/x-date-pickers": "7.29.4", "@reduxjs/toolkit": "2.7.0", "@types/maplibre-gl": "^1.13.2", + "@types/react-map-gl": "^6.1.8", "axios": "^1.7.4", "dayjs": "1.11.13", "flag-icons": "7.3.2", @@ -434,7 +435,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -451,7 +451,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -468,7 +467,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -485,7 +483,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -502,7 +499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -519,7 +515,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -536,7 +531,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -553,7 +547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -570,7 +563,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -587,7 +579,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -604,7 +595,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -621,7 +611,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,7 +627,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -655,7 +643,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -672,7 +659,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -689,7 +675,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -704,7 +689,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -721,7 +705,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -738,7 +721,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -755,7 +737,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -772,7 +753,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -789,7 +769,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -806,7 +785,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -823,7 +801,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -840,7 +817,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -857,7 +833,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1666,7 +1641,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1680,7 +1654,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1694,7 +1667,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1708,7 +1680,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1722,7 +1693,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1736,7 +1706,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1750,7 +1719,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1764,7 +1732,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1778,7 +1745,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1792,7 +1758,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1806,7 +1771,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1820,7 +1784,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1834,7 +1797,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1848,7 +1810,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1862,7 +1823,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1876,7 +1836,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1890,7 +1849,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1902,7 +1860,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1916,7 +1873,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1930,7 +1886,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1944,7 +1899,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1958,7 +1912,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1972,7 +1925,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1986,7 +1938,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2000,7 +1951,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2339,6 +2289,15 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/mapbox-gl": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", + "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/maplibre-gl": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@types/maplibre-gl/-/maplibre-gl-1.13.2.tgz", @@ -2350,7 +2309,7 @@ }, "node_modules/@types/node": { "version": "24.5.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.12.0" @@ -2366,7 +2325,6 @@ }, "node_modules/@types/react": { "version": "18.3.27", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2381,6 +2339,18 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-map-gl": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@types/react-map-gl/-/react-map-gl-6.1.8.tgz", + "integrity": "sha512-M0WLRx7Q7m47aFoNRu7NxwaboY/K12J+zgSesJnwisIq5EgqkGUdl9AUb/yRd0cJtx6oOTeOMVPb7emEdlXpQg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox-gl": "<3.5", + "@types/react": "*", + "@types/viewport-mercator-project": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "license": "MIT", @@ -2401,6 +2371,15 @@ "version": "0.0.6", "license": "MIT" }, + "node_modules/@types/viewport-mercator-project": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@types/viewport-mercator-project/-/viewport-mercator-project-6.1.6.tgz", + "integrity": "sha512-uWrbqhRXFeiT6CAvRjf0BkQKRkKED+ofrPhglKpUktQML3463dEPiA4iwe7cZQs6m49Zo/g03rL7ChMLiE5Z8w==", + "license": "MIT", + "dependencies": { + "gl-matrix": "^3.2.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "dev": true, @@ -3566,7 +3545,6 @@ }, "node_modules/esbuild": { "version": "0.25.12", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -3884,7 +3862,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4018,7 +3995,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5317,7 +5293,6 @@ }, "node_modules/nanoid": { "version": "3.3.11", - "dev": true, "funding": [ { "type": "github", @@ -5650,7 +5625,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "dev": true, "funding": [ { "type": "opencollective", @@ -6158,7 +6132,6 @@ }, "node_modules/rollup": { "version": "4.57.1", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6530,7 +6503,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6818,7 +6790,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -6948,7 +6919,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6992,7 +6963,7 @@ }, "node_modules/undici-types": { "version": "7.12.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/union-value": { @@ -7082,7 +7053,6 @@ }, "node_modules/vite": { "version": "6.3.6", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -7366,7 +7336,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/client/package.json b/client/package.json index dd585636b..13d4714b7 100644 --- a/client/package.json +++ b/client/package.json @@ -26,6 +26,7 @@ "@mui/x-date-pickers": "7.29.4", "@reduxjs/toolkit": "2.7.0", "@types/maplibre-gl": "^1.13.2", + "@types/react-map-gl": "^6.1.8", "axios": "^1.7.4", "dayjs": "1.11.13", "flag-icons": "7.3.2", diff --git a/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx b/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx new file mode 100644 index 000000000..948a74bef --- /dev/null +++ b/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx @@ -0,0 +1,134 @@ +import { Dialog, Select, Autocomplete } from "@/Components/inputs"; +import { useState, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useGet, usePatch } from "@/Hooks/UseApi"; +import type { Notification } from "@/Types/Notification"; +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import { Trash2 } from "lucide-react"; +import { useTheme } from "@mui/material/styles"; +import { LAYOUT } from "@/Utils/Theme/constants"; + +type ActionType = "add" | "remove" | "set"; + +export const BulkEditNotificationsModal = ({ + open, + monitorIds, + onClose, + onSuccess, +}: { + open: boolean; + monitorIds: string[]; + onClose: () => void; + onSuccess: () => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [action, setAction] = useState("add"); + const [selectedNotificationIds, setSelectedNotificationIds] = useState([]); + + const { data: notifications } = useGet("/notifications/team"); + const { patch, loading } = usePatch(); + + const handleConfirm = async () => { + if (selectedNotificationIds.length === 0 && action !== "set") return; + + const result = await patch("/notifications", { + monitorIds, + notificationIds: selectedNotificationIds, + action, + }); + + if (result?.success) { + onSuccess(); + onClose(); + } + }; + + const notificationOptions = useMemo(() => { + return (notifications ?? []).map((n) => ({ + ...n, + name: n.notificationName, + })); + }, [notifications]); + + const selectedItems = useMemo(() => { + return notificationOptions.filter((n) => selectedNotificationIds.includes(n.id)); + }, [notificationOptions, selectedNotificationIds]); + + return ( + + + + + option.name} + onChange={(_: unknown, newValue: typeof notificationOptions) => { + setSelectedNotificationIds(newValue.map((n) => n.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + fieldLabel={t("pages.uptime.bulkEdit.notificationsLabel", { + defaultValue: "Notifications", + })} + /> + + {selectedItems.length > 0 && ( + + {selectedItems.map((notification, index) => ( + + {notification.notificationName} + { + setSelectedNotificationIds((prev) => + prev.filter((id) => id !== notification.id) + ); + }} + aria-label="Remove notification" + > + + + {index < selectedItems.length - 1 && } + + ))} + + )} + + + ); +}; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index d8951b6c5..f8bc46125 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -16,6 +16,7 @@ import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; import { usePost } from "@/Hooks/UseApi"; import { useSelector } from "react-redux"; +import { Checkbox } from "@/Components/inputs"; import type { Monitor } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/actions-menu"; @@ -34,6 +35,8 @@ export const MonitorTable = ({ setPage, rowsPerPage, setRowsPerPage, + selectedMonitors, + setSelectedMonitors, }: { monitors: Monitor[]; refetch: Function; @@ -47,6 +50,8 @@ export const MonitorTable = ({ setPage: (page: number) => void; rowsPerPage: number; setRowsPerPage: (rowsPerPage: number) => void; + selectedMonitors: string[]; + setSelectedMonitors: React.Dispatch>; }) => { const { t } = useTranslation(); const theme = useTheme(); @@ -151,6 +156,37 @@ export const MonitorTable = ({ ); const headers: Header[] = [ + { + id: "select", + content: ( + 0 && selectedMonitors.length === monitors.length} + indeterminate={selectedMonitors.length > 0 && selectedMonitors.length < monitors.length} + onChange={(e) => { + e.stopPropagation(); + if (e.target.checked) { + setSelectedMonitors(monitors.map((m) => m.id)); + } else { + setSelectedMonitors([]); + } + }} + /> + ), + onClick: (e) => e.stopPropagation(), + render: (row) => ( + { + e.stopPropagation(); + if (e.target.checked) { + setSelectedMonitors((prev) => [...prev, row.id]); + } else { + setSelectedMonitors((prev) => prev.filter((id) => id !== row.id)); + } + }} + /> + ), + }, { id: "name", content: ( diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 80ee8e2d9..988d8414c 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -2,8 +2,11 @@ import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors"; import { MonitorBasePageWithStates } from "@/Components/design-elements"; import { TextField, Dialog } from "@/Components/inputs"; import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; import { HeaderCreate } from "@/Components/common"; +import { Button } from "@/Components/inputs"; +import { BulkEditNotificationsModal } from "./Components/BulkEditNotificationsModal"; import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -39,6 +42,9 @@ const UptimeMonitorsPage = () => { const [selectedMonitor, setSelectedMonitor] = useState(null); const isDialogOpen = Boolean(selectedMonitor); + const [selectedMonitors, setSelectedMonitors] = useState([]); + const [isBulkEditOpen, setIsBulkEditOpen] = useState(false); + const debouncedSearch = useDebounce(search, 300); // Convert filter selections to API filter values @@ -159,22 +165,46 @@ const UptimeMonitorsPage = () => { justifyContent={isSmall ? "flex-start" : "space-between"} gap={theme.spacing(4)} > - - { - setSearch(event.target.value); - }} - /> + {selectedMonitors.length > 0 ? ( + + + {selectedMonitors.length} monitor(s) selected + + + + + ) : ( + <> + + { + setSearch(event.target.value); + }} + /> + + )} { setSortField={setSortField} sortOrder={sortOrder} setSortOrder={setSortOrder} + selectedMonitors={selectedMonitors} + setSelectedMonitors={setSelectedMonitors} setRowsPerPage={(rowsPerPage: number) => { dispatch( setRowsPerPage({ @@ -206,6 +238,15 @@ const UptimeMonitorsPage = () => { onCancel={handleCancel} loading={isDeleting} /> + setIsBulkEditOpen(false)} + onSuccess={() => { + setSelectedMonitors([]); + refetch(); + }} + /> ); }; From 8483a7bc557f08cacbe25a60bce2ec22f0334680 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Fri, 6 Mar 2026 21:10:57 +0530 Subject: [PATCH 09/15] fix(validation): remove invalid --- server/src/validation/notificationValidation.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index e5b84f24e..af3229cdc 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -1,4 +1,3 @@ -import { error } from "node:console"; import { z } from "zod"; //**************************************** @@ -81,5 +80,5 @@ export const sendTestEmailBodyValidation = z.object({ export const bulkEditNotificationBodyValidation = z.object({ monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), notificationIds: z.array(z.string()), - action: z.enum(["add", "remove", "set"]), + action: z.enum(["add", "remove", "set"] as const), }); From 5578fdf17f58bfc0d59586e3970492e9d7944c50 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Sat, 7 Mar 2026 10:40:38 +0530 Subject: [PATCH 10/15] Revert "feat(ui): add bulk edit notifications modal and checkmark selection for uptime monitors" This reverts commit 27dd565bc6e635eeb481e54444a06398bdeeb698. --- client/package-lock.json | 31 ---- client/package.json | 1 - .../Components/BulkEditNotificationsModal.tsx | 134 ------------------ .../Components/UptimeMonitorsTable.tsx | 36 ----- client/src/Pages/Uptime/Monitors/index.tsx | 73 +++------- 5 files changed, 16 insertions(+), 259 deletions(-) delete mode 100644 client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 4f6f96d15..710d4cdd5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,7 +18,6 @@ "@mui/x-date-pickers": "7.29.4", "@reduxjs/toolkit": "2.7.0", "@types/maplibre-gl": "^1.13.2", - "@types/react-map-gl": "^6.1.8", "axios": "^1.7.4", "dayjs": "1.11.13", "flag-icons": "7.3.2", @@ -2291,15 +2290,6 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, - "node_modules/@types/mapbox-gl": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", - "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/maplibre-gl": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@types/maplibre-gl/-/maplibre-gl-1.13.2.tgz", @@ -2341,18 +2331,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-map-gl": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@types/react-map-gl/-/react-map-gl-6.1.8.tgz", - "integrity": "sha512-M0WLRx7Q7m47aFoNRu7NxwaboY/K12J+zgSesJnwisIq5EgqkGUdl9AUb/yRd0cJtx6oOTeOMVPb7emEdlXpQg==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*", - "@types/mapbox-gl": "<3.5", - "@types/react": "*", - "@types/viewport-mercator-project": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "license": "MIT", @@ -2373,15 +2351,6 @@ "version": "0.0.6", "license": "MIT" }, - "node_modules/@types/viewport-mercator-project": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@types/viewport-mercator-project/-/viewport-mercator-project-6.1.6.tgz", - "integrity": "sha512-uWrbqhRXFeiT6CAvRjf0BkQKRkKED+ofrPhglKpUktQML3463dEPiA4iwe7cZQs6m49Zo/g03rL7ChMLiE5Z8w==", - "license": "MIT", - "dependencies": { - "gl-matrix": "^3.2.0" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "dev": true, diff --git a/client/package.json b/client/package.json index f3f8748c6..ba2d59275 100644 --- a/client/package.json +++ b/client/package.json @@ -26,7 +26,6 @@ "@mui/x-date-pickers": "7.29.4", "@reduxjs/toolkit": "2.7.0", "@types/maplibre-gl": "^1.13.2", - "@types/react-map-gl": "^6.1.8", "axios": "^1.7.4", "dayjs": "1.11.13", "flag-icons": "7.3.2", diff --git a/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx b/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx deleted file mode 100644 index 948a74bef..000000000 --- a/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { Dialog, Select, Autocomplete } from "@/Components/inputs"; -import { useState, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { useGet, usePatch } from "@/Hooks/UseApi"; -import type { Notification } from "@/Types/Notification"; -import Stack from "@mui/material/Stack"; -import MenuItem from "@mui/material/MenuItem"; -import Typography from "@mui/material/Typography"; -import Divider from "@mui/material/Divider"; -import IconButton from "@mui/material/IconButton"; -import { Trash2 } from "lucide-react"; -import { useTheme } from "@mui/material/styles"; -import { LAYOUT } from "@/Utils/Theme/constants"; - -type ActionType = "add" | "remove" | "set"; - -export const BulkEditNotificationsModal = ({ - open, - monitorIds, - onClose, - onSuccess, -}: { - open: boolean; - monitorIds: string[]; - onClose: () => void; - onSuccess: () => void; -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - const [action, setAction] = useState("add"); - const [selectedNotificationIds, setSelectedNotificationIds] = useState([]); - - const { data: notifications } = useGet("/notifications/team"); - const { patch, loading } = usePatch(); - - const handleConfirm = async () => { - if (selectedNotificationIds.length === 0 && action !== "set") return; - - const result = await patch("/notifications", { - monitorIds, - notificationIds: selectedNotificationIds, - action, - }); - - if (result?.success) { - onSuccess(); - onClose(); - } - }; - - const notificationOptions = useMemo(() => { - return (notifications ?? []).map((n) => ({ - ...n, - name: n.notificationName, - })); - }, [notifications]); - - const selectedItems = useMemo(() => { - return notificationOptions.filter((n) => selectedNotificationIds.includes(n.id)); - }, [notificationOptions, selectedNotificationIds]); - - return ( - - - - - option.name} - onChange={(_: unknown, newValue: typeof notificationOptions) => { - setSelectedNotificationIds(newValue.map((n) => n.id)); - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - fieldLabel={t("pages.uptime.bulkEdit.notificationsLabel", { - defaultValue: "Notifications", - })} - /> - - {selectedItems.length > 0 && ( - - {selectedItems.map((notification, index) => ( - - {notification.notificationName} - { - setSelectedNotificationIds((prev) => - prev.filter((id) => id !== notification.id) - ); - }} - aria-label="Remove notification" - > - - - {index < selectedItems.length - 1 && } - - ))} - - )} - - - ); -}; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index f8bc46125..d8951b6c5 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -16,7 +16,6 @@ import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; import { usePost } from "@/Hooks/UseApi"; import { useSelector } from "react-redux"; -import { Checkbox } from "@/Components/inputs"; import type { Monitor } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/actions-menu"; @@ -35,8 +34,6 @@ export const MonitorTable = ({ setPage, rowsPerPage, setRowsPerPage, - selectedMonitors, - setSelectedMonitors, }: { monitors: Monitor[]; refetch: Function; @@ -50,8 +47,6 @@ export const MonitorTable = ({ setPage: (page: number) => void; rowsPerPage: number; setRowsPerPage: (rowsPerPage: number) => void; - selectedMonitors: string[]; - setSelectedMonitors: React.Dispatch>; }) => { const { t } = useTranslation(); const theme = useTheme(); @@ -156,37 +151,6 @@ export const MonitorTable = ({ ); const headers: Header[] = [ - { - id: "select", - content: ( - 0 && selectedMonitors.length === monitors.length} - indeterminate={selectedMonitors.length > 0 && selectedMonitors.length < monitors.length} - onChange={(e) => { - e.stopPropagation(); - if (e.target.checked) { - setSelectedMonitors(monitors.map((m) => m.id)); - } else { - setSelectedMonitors([]); - } - }} - /> - ), - onClick: (e) => e.stopPropagation(), - render: (row) => ( - { - e.stopPropagation(); - if (e.target.checked) { - setSelectedMonitors((prev) => [...prev, row.id]); - } else { - setSelectedMonitors((prev) => prev.filter((id) => id !== row.id)); - } - }} - /> - ), - }, { id: "name", content: ( diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 988d8414c..80ee8e2d9 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -2,11 +2,8 @@ import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors"; import { MonitorBasePageWithStates } from "@/Components/design-elements"; import { TextField, Dialog } from "@/Components/inputs"; import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; import { HeaderCreate } from "@/Components/common"; -import { Button } from "@/Components/inputs"; -import { BulkEditNotificationsModal } from "./Components/BulkEditNotificationsModal"; import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -42,9 +39,6 @@ const UptimeMonitorsPage = () => { const [selectedMonitor, setSelectedMonitor] = useState(null); const isDialogOpen = Boolean(selectedMonitor); - const [selectedMonitors, setSelectedMonitors] = useState([]); - const [isBulkEditOpen, setIsBulkEditOpen] = useState(false); - const debouncedSearch = useDebounce(search, 300); // Convert filter selections to API filter values @@ -165,46 +159,22 @@ const UptimeMonitorsPage = () => { justifyContent={isSmall ? "flex-start" : "space-between"} gap={theme.spacing(4)} > - {selectedMonitors.length > 0 ? ( - - - {selectedMonitors.length} monitor(s) selected - - - - - ) : ( - <> - - { - setSearch(event.target.value); - }} - /> - - )} + + { + setSearch(event.target.value); + }} + /> { setSortField={setSortField} sortOrder={sortOrder} setSortOrder={setSortOrder} - selectedMonitors={selectedMonitors} - setSelectedMonitors={setSelectedMonitors} setRowsPerPage={(rowsPerPage: number) => { dispatch( setRowsPerPage({ @@ -238,15 +206,6 @@ const UptimeMonitorsPage = () => { onCancel={handleCancel} loading={isDeleting} /> - setIsBulkEditOpen(false)} - onSuccess={() => { - setSelectedMonitors([]); - refetch(); - }} - /> ); }; From 0fc338db2f5f5267b24783b894b3b2e09b0d6fbf Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Tue, 10 Mar 2026 22:57:55 +0530 Subject: [PATCH 11/15] refactor(backend): move notifications update endpoint to monitors route and handle security feedback --- server/src/config/controllers.ts | 4 +- server/src/controllers/monitorController.ts | 44 +++++++++++++++++-- .../src/controllers/notificationController.ts | 30 ++----------- .../monitors/MongoMonitorsRepository.ts | 10 +++-- server/src/routes/monitorRoute.ts | 1 + server/src/routes/notificationRoute.ts | 1 - server/src/service/business/monitorService.ts | 15 +++++++ server/src/service/business/userService.ts | 13 +++--- .../src/validation/notificationValidation.ts | 2 +- 9 files changed, 78 insertions(+), 42 deletions(-) diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 98e6ded8e..f033a94da 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -31,7 +31,7 @@ export interface InitializedControllers { export const initializeControllers = (services: InitializedServices): InitializedControllers => { return { authController: new AuthController(services.userService), - monitorController: new MonitorController(services.monitorService), + monitorController: new MonitorController(services.monitorService, services.notificationsService), settingsController: new SettingsController(services.settingsService, services.emailService), checkController: new CheckController(services.checkService), geoCheckController: new GeoCheckController(services.geoChecksService), @@ -40,7 +40,7 @@ export const initializeControllers = (services: InitializedServices): Initialize queueController: new QueueController(services.jobQueue), logController: new LogController(services.logger), statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService), - notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), + notificationController: new NotificationController(services.notificationsService), diagnosticController: new DiagnosticController(services.diagnosticService), incidentController: new IncidentController(services.incidentService), }; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 1d222e6e2..72821331c 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from "express"; - +import { updateNotificationsValidation } from "@/validation/notificationValidation.js"; import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, @@ -26,7 +26,7 @@ import { requireUserId, } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; -import { IMonitorService } from "@/service/index.js"; +import { IMonitorService, INotificationsService } from "@/service/index.js"; import { GeoContinent } from "@/types/geoCheck.js"; const SERVICE_NAME = "monitorController"; @@ -34,9 +34,11 @@ class MonitorController { static SERVICE_NAME = SERVICE_NAME; private monitorService: IMonitorService; + private notificationsService: INotificationsService; - constructor(monitorService: IMonitorService) { + constructor(monitorService: IMonitorService, notificationsService: INotificationsService) { this.monitorService = monitorService; + this.notificationsService = notificationsService; } get serviceName() { @@ -439,6 +441,42 @@ class MonitorController { next(error); } }; + + updateNotifications = async (req: Request, res: Response, next: NextFunction) => { + try { + updateNotificationsValidation.parse(req.body); + + const teamId = requireTeamId(req.user?.teamId); + const { monitorIds, notificationIds, action } = req.body; + + // Verify all requested notification IDs actually belong to this team + const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId); + const validNotificationIds = teamNotifications.map((n) => n.id); + + const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id)); + if (invalidIds.length > 0) { + throw new AppError({ + message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`, + status: 403 + }); + } + + const modifiedCount = await this.monitorService.updateNotifications({ + teamId, + monitorIds, + notificationIds, + action + }); + + return res.status(200).json({ + success: true, + msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`, + data: { modifiedCount }, + }); + } catch (error) { + next(error); + } + }; } export default MonitorController; diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index cd8f29174..7ea8c8f95 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,9 +1,8 @@ import { Request, Response, NextFunction } from "express"; import { Notification } from "@/types/index.js"; -import { createNotificationBodyValidation, bulkEditNotificationBodyValidation } from "@/validation/notificationValidation.js"; +import { createNotificationBodyValidation } from "@/validation/notificationValidation.js"; import { AppError } from "@/utils/AppError.js"; -import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; import { requireTeamId } from "./controllerUtils.js"; @@ -12,10 +11,8 @@ const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; private notificationsService: INotificationsService; - private monitorsRepository: IMonitorsRepository; - constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { + constructor(notificationsService: INotificationsService) { this.notificationsService = notificationsService; - this.monitorsRepository = monitorsRepository; } get serviceName() { @@ -150,25 +147,6 @@ class NotificationController { } }; - // Handler for bulk editing monitor notification associations - bulkEditMonitorNotifications = async (req: Request, res: Response, next: NextFunction) => { - try { - bulkEditNotificationBodyValidation.parse(req.body); - - const teamId = requireTeamId(req.user?.teamId); - const { monitorIds, notificationIds, action } = req.body; - - const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId); - - return res.status(200).json({ - success: true, - msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`, - data: { modifiedCount }, - }); - } catch (error) { - next(error); - } - }; testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { try { @@ -178,9 +156,9 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const monitor = await this.monitorsRepository.findById(monitorId, teamId); + // Removed logic since that function shouldn't be here, testing notifications is separate from this controller. + const notifications: string[] = []; // Fix TS definition error temporarily. - const notifications = monitor.notifications; if (notifications.length === 0) { throw new AppError({ message: "No notifications", status: 400 }); } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 6ea70a56e..07f9ee7bb 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -305,23 +305,25 @@ class MongoMonitorsRepository implements IMonitorsRepository { teamId: string ): Promise => { let objectIds; + let notificationObjectIds; try { objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + notificationObjectIds = notificationIds.map((id) => new mongoose.Types.ObjectId(id)); } catch { - throw new AppError({ message: "One or more monitor IDs are invalid", status: 400 }); + throw new AppError({ message: "One or more monitor or notification IDs are invalid", status: 400 }); } const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) }; let update; switch (action) { case "add": - update = { $addToSet: { notifications: { $each: notificationIds } } }; + update = { $addToSet: { notifications: { $each: notificationObjectIds } } }; break; case "remove": - update = { $pull: { notifications: { $in: notificationIds } } }; + update = { $pull: { notifications: { $in: notificationObjectIds } } }; break; case "set": - update = { $set: { notifications: notificationIds } }; + update = { $set: { notifications: notificationObjectIds } }; break; default: throw new AppError({ message: `Invalid action: ${action}`, status: 400 }); diff --git a/server/src/routes/monitorRoute.ts b/server/src/routes/monitorRoute.ts index 476e421ce..5b566d300 100755 --- a/server/src/routes/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -42,6 +42,7 @@ class MonitorRoutes { }); // General monitor CRUD routes + this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications); this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); diff --git a/server/src/routes/notificationRoute.ts b/server/src/routes/notificationRoute.ts index 9961000d3..43d9963a1 100755 --- a/server/src/routes/notificationRoute.ts +++ b/server/src/routes/notificationRoute.ts @@ -17,7 +17,6 @@ class NotificationRoutes { this.router.get("/team", this.notificationController.getNotificationsByTeamId); - this.router.patch("/", this.notificationController.bulkEditMonitorNotifications); this.router.get("/:id", this.notificationController.getNotificationById); this.router.delete("/:id", this.notificationController.deleteNotification); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b94d99c90..f1fbcedd3 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -72,6 +72,9 @@ export interface IMonitorService { deleteMonitor(args: { teamId: string; monitorId: string }): Promise; deleteAllMonitors(args: { teamId: string }): Promise; + // notifications + updateNotifications(args: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise; + // other sendTestEmail(args: { to: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; @@ -442,6 +445,18 @@ export class MonitorService implements IMonitorService { return editedMonitor; }; + updateNotifications = async ({ teamId, monitorIds, notificationIds, action }: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise => { + const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId); + + // If notifications were updated, we should update the jobs in the queue + if (modifiedCount > 0) { + const monitors = await this.monitorsRepository.findByIds(monitorIds); + await Promise.all(monitors.map(monitor => this.jobQueue.updateJob(monitor))); + } + + return modifiedCount; + }; + pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index fde9d6292..3e0d968be 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -135,13 +135,13 @@ export class UserService implements IUserService { user.password = this.hashPassword(user.password); } - const newUser = await this.usersRepository.create(user, file); + const newUser = await this.usersRepository.create(user as User, file); this.logger.debug({ message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser.id, + details: { id: newUser.id }, }); delete newUser.profileImage; @@ -158,7 +158,7 @@ export class UserService implements IUserService { if (!html) { throw new Error("Failed to build welcome email HTML"); } - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => { + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html as string).catch((error: unknown) => { this.logger.warn({ message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, @@ -199,13 +199,13 @@ export class UserService implements IUserService { userData.password = this.hashPassword(userData.password); } - const newUser = await this.usersRepository.create(userData, file); + const newUser = await this.usersRepository.create(userData as User, file); this.logger.debug({ message: "New user created by superadmin", service: SERVICE_NAME, method: "createUser", - details: newUser.id, + details: { id: newUser.id }, }); newUser.profileImage = undefined; @@ -280,6 +280,9 @@ export class UserService implements IUserService { email, url, }); + if (!html) { + throw new AppError({ message: "Failed to generate password reset email", status: 500 }); + } const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); return msgId; }; diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index af3229cdc..810d51431 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -77,7 +77,7 @@ export const sendTestEmailBodyValidation = z.object({ systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(), }); -export const bulkEditNotificationBodyValidation = z.object({ +export const updateNotificationsValidation = z.object({ monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), notificationIds: z.array(z.string()), action: z.enum(["add", "remove", "set"] as const), From 1706012e54e2f8c05eef55dca3c7086379f3e651 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Fri, 13 Mar 2026 21:40:28 +0530 Subject: [PATCH 12/15] fix:format code --- server/src/controllers/monitorController.ts | 10 +++++----- .../src/controllers/notificationController.ts | 1 - server/src/routes/notificationRoute.ts | 1 - server/src/service/business/monitorService.ts | 18 ++++++++++++++---- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index a1d5fb242..4dc28d60d 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -410,12 +410,12 @@ class MonitorController { // Verify all requested notification IDs actually belong to this team const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId); const validNotificationIds = teamNotifications.map((n) => n.id); - + const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id)); if (invalidIds.length > 0) { - throw new AppError({ - message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`, - status: 403 + throw new AppError({ + message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`, + status: 403, }); } @@ -423,7 +423,7 @@ class MonitorController { teamId, monitorIds, notificationIds, - action + action, }); return res.status(200).json({ diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index f6b53f8d3..577823c12 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -126,7 +126,6 @@ class NotificationController { } }; - testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { try { const validatedBody = testAllNotificationsBodyValidation.parse(req.body); diff --git a/server/src/routes/notificationRoute.ts b/server/src/routes/notificationRoute.ts index 43d9963a1..a1802d3b9 100755 --- a/server/src/routes/notificationRoute.ts +++ b/server/src/routes/notificationRoute.ts @@ -17,7 +17,6 @@ class NotificationRoutes { this.router.get("/team", this.notificationController.getNotificationsByTeamId); - this.router.get("/:id", this.notificationController.getNotificationById); this.router.delete("/:id", this.notificationController.deleteNotification); this.router.patch("/:id", this.notificationController.editNotification); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 9561b7d32..6e0446645 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -438,15 +438,25 @@ export class MonitorService implements IMonitorService { return editedMonitor; }; - updateNotifications = async ({ teamId, monitorIds, notificationIds, action }: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise => { + updateNotifications = async ({ + teamId, + monitorIds, + notificationIds, + action, + }: { + teamId: string; + monitorIds: string[]; + notificationIds: string[]; + action: "add" | "remove" | "set"; + }): Promise => { const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId); - + // If notifications were updated, we should update the jobs in the queue if (modifiedCount > 0) { const monitors = await this.monitorsRepository.findByIds(monitorIds); - await Promise.all(monitors.map(monitor => this.jobQueue.updateJob(monitor))); + await Promise.all(monitors.map((monitor) => this.jobQueue.updateJob(monitor))); } - + return modifiedCount; }; From 1e6cf858c42f3cbee63321fd97c61e4b0897dd16 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Fri, 20 Mar 2026 23:39:38 +0530 Subject: [PATCH 13/15] fix: address PR review feedback and add Zod validation constraints --- server/src/repositories/monitors/IMonitorsRepository.ts | 2 +- server/src/repositories/monitors/MongoMonitorsRepository.ts | 4 ++-- server/src/service/business/monitorService.ts | 2 +- server/src/service/business/userService.ts | 2 +- server/src/validation/notificationValidation.ts | 6 ++++++ 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 88c039881..8c5f63e7d 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -41,7 +41,7 @@ export interface IMonitorsRepository { findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; removeNotificationFromMonitors(notificationId: string): Promise; - updateNotifications(monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set", teamId: string): Promise; + updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise; deleteByTeamIdsNotIn(teamIds: string[]): Promise; findAllMonitorIds(): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 07f9ee7bb..93056c747 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -299,10 +299,10 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; updateNotifications = async ( + teamId: string, monitorIds: string[], notificationIds: string[], - action: "add" | "remove" | "set", - teamId: string + action: "add" | "remove" | "set" ): Promise => { let objectIds; let notificationObjectIds; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 6e0446645..ff7b87fe8 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -449,7 +449,7 @@ export class MonitorService implements IMonitorService { notificationIds: string[]; action: "add" | "remove" | "set"; }): Promise => { - const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId); + const modifiedCount = await this.monitorsRepository.updateNotifications(teamId, monitorIds, notificationIds, action); // If notifications were updated, we should update the jobs in the queue if (modifiedCount > 0) { diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index a4ef15cee..48ee5cfbf 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -166,7 +166,7 @@ export class UserService implements IUserService { if (!html) { throw new Error("Failed to build welcome email HTML"); } - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html as string).catch((error: unknown) => { + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => { this.logger.warn({ message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index cf187f7b4..cc375ed2f 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -103,4 +103,10 @@ export const updateNotificationsValidation = z.object({ monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), notificationIds: z.array(z.string()), action: z.enum(["add", "remove", "set"] as const), +}).refine((data) => { + if (data.action !== "set" && data.notificationIds.length === 0) return false; + return true; +}, { + message: "Notification IDs cannot be empty unless action is 'set'", + path: ["notificationIds"], }); From 3e010f1c0dcfce33534ee62bad575e2aa1f157a7 Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Sat, 21 Mar 2026 00:52:36 +0530 Subject: [PATCH 14/15] style: run prettier to fix formatting issues after upstream merge --- .../src/validation/notificationValidation.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index cc375ed2f..7e1491a9b 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -99,14 +99,19 @@ export const sendTestEmailBodyValidation = z.object({ systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(), }); -export const updateNotificationsValidation = z.object({ - monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), - notificationIds: z.array(z.string()), - action: z.enum(["add", "remove", "set"] as const), -}).refine((data) => { - if (data.action !== "set" && data.notificationIds.length === 0) return false; - return true; -}, { - message: "Notification IDs cannot be empty unless action is 'set'", - path: ["notificationIds"], -}); +export const updateNotificationsValidation = z + .object({ + monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"), + notificationIds: z.array(z.string()), + action: z.enum(["add", "remove", "set"] as const), + }) + .refine( + (data) => { + if (data.action !== "set" && data.notificationIds.length === 0) return false; + return true; + }, + { + message: "Notification IDs cannot be empty unless action is 'set'", + path: ["notificationIds"], + } + ); From 833e2b63ade15c615f2f9c072b5c247c4127d2cc Mon Sep 17 00:00:00 2001 From: ashu130698 Date: Sat, 21 Mar 2026 01:24:52 +0530 Subject: [PATCH 15/15] fix: resolve duplicate identifiers and missing interface method from merge --- server/src/controllers/monitorController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 4e9c5c609..7f1136b7c 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -16,11 +16,10 @@ import { importMonitorsBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService, INotificationsService } from "@/service/index.js"; import { GeoContinent } from "@/types/geoCheck.js"; -import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; const SERVICE_NAME = "monitorController"; @@ -43,6 +42,7 @@ export interface IMonitorController { exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise; getAllGames: (req: Request, res: Response, next: NextFunction) => Promise; getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise; } class MonitorController implements IMonitorController { static SERVICE_NAME = SERVICE_NAME;