diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index 3164a111b..7cb642977 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -1,4 +1,7 @@ -import { triggerNotificationBodyValidation } from "../validation/joi.js"; +import { + triggerNotificationBodyValidation, + createNotificationBodyValidation, +} from "../validation/joi.js"; import { handleError, handleValidationError } from "./controllerUtils.js"; const SERVICE_NAME = "NotificationController"; @@ -15,10 +18,11 @@ const PLATFORMS = { }; class NotificationController { - constructor(notificationService, stringService, statusService) { + constructor({ notificationService, stringService, statusService, db }) { this.notificationService = notificationService; this.stringService = stringService; this.statusService = statusService; + this.db = db; this.triggerNotification = this.triggerNotification.bind(this); this.testWebhook = this.testWebhook.bind(this); } @@ -169,6 +173,50 @@ class NotificationController { next(handleError(error, SERVICE_NAME, "testWebhook")); } } + + createNotification = async (req, res, next) => { + try { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + + try { + const notification = await this.db.createNotification(req.body); + return res.success({ + msg: "Notification created successfully", + data: notification, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "createNotification")); + } + }; + + getNotificationsByTeamId = async (req, res, next) => { + try { + const notifications = await this.db.getNotificationsByTeamId(req.params.teamId); + return res.success({ + msg: "Notifications fetched successfully", + data: notifications, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getNotificationsByTeamId")); + } + }; + + deleteNotification = async (req, res, next) => { + try { + await this.db.deleteNotificationById(req.params.id); + return res.success({ + msg: "Notification deleted successfully", + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "deleteNotification")); + } + }; } export default NotificationController; diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js index ab51e0617..27f2f932a 100755 --- a/server/db/models/Notification.js +++ b/server/db/models/Notification.js @@ -5,20 +5,32 @@ const configSchema = mongoose.Schema( webhookUrl: { type: String }, botToken: { type: String }, chatId: { type: String }, + platform: { + type: String, + enum: ["slack", "pager_duty"], + }, + routingKey: { type: String }, }, { _id: false } ); const NotificationSchema = mongoose.Schema( { - monitorId: { + userId: { type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", + ref: "User", immutable: true, + required: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, }, type: { type: String, - enum: ["email", "sms", "webhook"], + enum: ["email", "sms", "webhook", "pager_duty"], }, platform: { type: String, @@ -27,6 +39,10 @@ const NotificationSchema = mongoose.Schema( type: configSchema, default: () => ({}), }, + notificationName: { + type: String, + required: true, + }, address: { type: String, }, diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js index aab2d03bd..d172ee8cf 100755 --- a/server/db/mongo/modules/notificationModule.js +++ b/server/db/mongo/modules/notificationModule.js @@ -1,4 +1,5 @@ import Notification from "../../models/Notification.js"; +import Monitor from "../../models/Monitor.js"; const SERVICE_NAME = "notificationModule"; /** * Creates a new notification. @@ -21,6 +22,28 @@ const createNotification = async (notificationData) => { } }; +const getNotificationsByTeamId = async (teamId) => { + try { + const notifications = await Notification.find({ teamId }); + return notifications; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNotificationsByTeamId"; + throw error; + } +}; + +const getNotificationsByIds = async (notificationIds) => { + try { + const notifications = await Notification.find({ _id: { $in: notificationIds } }); + return notifications; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getNotificationsByIds"; + throw error; + } +}; + /** * Retrieves notifications by monitor ID. * @param {mongoose.Types.ObjectId} monitorId - The ID of the monitor. @@ -49,8 +72,23 @@ const deleteNotificationsByMonitorId = async (monitorId) => { } }; +const deleteNotificationById = async (id) => { + try { + const result = await Notification.findByIdAndDelete(id); + await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); + return result; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteNotificationById"; + throw error; + } +}; + export { createNotification, + getNotificationsByTeamId, + getNotificationsByIds, getNotificationsByMonitorId, deleteNotificationsByMonitorId, + deleteNotificationById, }; diff --git a/server/index.js b/server/index.js index 2d48d753f..b520e6171 100755 --- a/server/index.js +++ b/server/index.js @@ -291,11 +291,12 @@ const startApp = async () => { ServiceRegistry.get(StringService.SERVICE_NAME) ); - const notificationController = new NotificationController( - ServiceRegistry.get(NotificationService.SERVICE_NAME), - ServiceRegistry.get(StringService.SERVICE_NAME), - ServiceRegistry.get(StatusService.SERVICE_NAME) - ); + const notificationController = new NotificationController({ + notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME), + stringService: ServiceRegistry.get(StringService.SERVICE_NAME), + statusService: ServiceRegistry.get(StatusService.SERVICE_NAME), + db: ServiceRegistry.get(MongoDB.SERVICE_NAME), + }); const diagnosticController = new DiagnosticController( ServiceRegistry.get(MongoDB.SERVICE_NAME) diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js index 2c0bbf5b7..fe6b1c2dd 100755 --- a/server/routes/notificationRoute.js +++ b/server/routes/notificationRoute.js @@ -14,6 +14,15 @@ class NotificationRoutes { this.router.post("/trigger", this.notificationController.triggerNotification); this.router.post("/test-webhook", this.notificationController.testWebhook); + + this.router.post("/", this.notificationController.createNotification); + + this.router.get( + "/team/:teamId", + this.notificationController.getNotificationsByTeamId + ); + + this.router.delete("/:id", this.notificationController.deleteNotification); } getRouter() { diff --git a/server/service/notificationService.js b/server/service/notificationService.js index ed74ff539..878881334 100755 --- a/server/service/notificationService.js +++ b/server/service/notificationService.js @@ -112,8 +112,7 @@ class NotificationService { async sendWebhookNotification(networkResponse, notification) { const { monitor, status, code } = networkResponse; - const { platform } = notification; - const { webhookUrl, botToken, chatId } = notification.config; + const { webhookUrl, platform, botToken, chatId } = notification.config; // Early return if platform is not supported if (!PLATFORM_TYPES.includes(platform)) { @@ -204,6 +203,38 @@ class NotificationService { return true; } + async sendPagerDutyNotification(networkResponse, notification) { + const { monitor, status, code } = networkResponse; + const { routingKey, platform } = notification.config; + + const message = this.formatNotificationMessage( + monitor, + status, + platform, + null, + code, // Pass the code field directly + networkResponse.timestamp + ); + + try { + const response = await this.networkService.requestPagerDuty({ + message, + routingKey, + monitorUrl: monitor.url, + }); + return response.status; + } catch (error) { + this.logger.error({ + message: "Failed to send PagerDuty notification", + details: error.details, + service: error.service || this.SERVICE_NAME, + method: error.method || "sendPagerDutyNotification", + stack: error.stack, + }); + return false; + } + } + async handleStatusNotifications(networkResponse) { try { // If status hasn't changed, we're done @@ -211,17 +242,17 @@ class NotificationService { // if prevStatus is undefined, monitor is resuming, we're done if (networkResponse.prevStatus === undefined) return false; - const notifications = await this.db.getNotificationsByMonitorId( - networkResponse.monitorId - ); + const notificationIDs = networkResponse.monitor?.notifications ?? []; + const notifications = await this.db.getNotificationsByIds(notificationIDs); for (const notification of notifications) { if (notification.type === "email") { await this.sendEmail(networkResponse, notification.address); } else if (notification.type === "webhook") { await this.sendWebhookNotification(networkResponse, notification); + } else if (notification.type === "pager_duty") { + await this.sendPagerDutyNotification(networkResponse, notification); } - // Handle other types of notifications here } return true; } catch (error) { diff --git a/server/validation/joi.js b/server/validation/joi.js index 9024c26a4..9d01922e1 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -556,6 +556,47 @@ const triggerNotificationBodyValidation = joi.object({ }), }); +const createNotificationBodyValidation = joi.object({ + userId: joi.string().required().messages({ + "number.empty": "User ID is required", + "any.required": "User ID is required", + }), + teamId: joi.string().required().messages({ + "string.empty": "Team ID is required", + "any.required": "Team ID is required", + }), + notificationName: joi.string().required().messages({ + "string.empty": "Notification name is required", + "any.required": "Notification name is required", + }), + address: joi.when("type", { + is: "email", + then: joi.string().email().required().messages({ + "string.empty": "E-mail address cannot be empty", + "any.required": "E-mail address is required", + "string.email": "Please enter a valid e-mail address", + }), + }), + type: joi.string().valid("email", "webhook", "pager_duty").required().messages({ + "string.empty": "Notification type is required", + "any.required": "Notification type is required", + "any.only": "Notification type must be email, webhook, or pager_duty", + }), + config: joi.when("type", { + is: "webhook", + then: joi.object({ + webhookUrl: joi.string().uri().required().messages({ + "string.uri": "Webhook URL must be a valid URI", + "any.required": "Webhook URL is required", + }), + platform: joi.string().required().messages({ + "string.empty": "Platform is required", + "any.required": "Platform is required", + }), + }), + }), +}); + //**************************************** // Announcetment Page Validation //**************************************** @@ -643,6 +684,7 @@ export { getStatusPageQueryValidation, imageValidation, triggerNotificationBodyValidation, + createNotificationBodyValidation, webhookConfigValidation, createAnnouncementValidation, sendTestEmailBodyValidation,