From 28f5d355a7450528a3c46e561533a61c00378719 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Wed, 22 Jan 2025 15:41:47 -0800 Subject: [PATCH 01/31] Add Discord webhook notification support --- Server/db/models/Notification.js | 2 +- Server/service/notificationService.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index 8767880c1..8d4a7e515 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -8,7 +8,7 @@ const NotificationSchema = mongoose.Schema( }, type: { type: String, - enum: ["email", "sms"], + enum: ["email", "sms", "discord"], }, address: { type: String, diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 807640a61..eade52f55 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -1,4 +1,5 @@ const SERVICE_NAME = "NotificationService"; +import axios from 'axios'; class NotificationService { static SERVICE_NAME = SERVICE_NAME; @@ -16,6 +17,26 @@ class NotificationService { this.logger = logger; } + async sendDiscordNotification(networkResponse, webhookUrl) { + const { monitor, status } = networkResponse; + const message = { + content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` + }; + + try { + await axios.post(webhookUrl, message); + return true; + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "sendDiscordNotification", + stack: error.stack, + }); + return false; + } + } + /** * Sends an email notification for hardware infrastructure alerts * From 172597e7687654531beea55ec71f6e3b572de639 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Wed, 22 Jan 2025 18:46:50 -0800 Subject: [PATCH 02/31] Add Slack webhook integration. --- Server/db/models/Notification.js | 2 +- Server/service/notificationService.js | 51 +++++++++++++++++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index 8d4a7e515..58433fbc0 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -8,7 +8,7 @@ const NotificationSchema = mongoose.Schema( }, type: { type: String, - enum: ["email", "sms", "discord"], + enum: ["email", "sms", "discord", "slack"], }, address: { type: String, diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index eade52f55..f47a2a74c 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -37,6 +37,26 @@ class NotificationService { } } + async sendSlackNotification(networkResponse, webhookUrl) { + const { monitor, status } = networkResponse; + const message = { + text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` + }; + + try { + await axios.post(webhookUrl, message); + return true; + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "sendSlackNotification", + stack: error.stack, + }); + return false; + } + } + /** * Sends an email notification for hardware infrastructure alerts * @@ -77,22 +97,25 @@ class NotificationService { } async handleStatusNotifications(networkResponse) { - try { - //If status hasn't changed, we're done - if (networkResponse.statusChanged === false) return false; + try { + // If status hasn't changed, we're done + if (networkResponse.statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (networkResponse.prevStatus === undefined) return false; - const notifications = await this.db.getNotificationsByMonitorId( - networkResponse.monitorId - ); + // if prevStatus is undefined, monitor is resuming, we're done + if (networkResponse.prevStatus === undefined) return false; - for (const notification of notifications) { - if (notification.type === "email") { - this.sendEmail(networkResponse, notification.address); - } - // Handle other types of notifications here - } + const notifications = await this.db.getNotificationsByMonitorId(networkResponse.monitorId); + + for (const notification of notifications) { + if (notification.type === "email") { + this.sendEmail(networkResponse, notification.address); + } else if (notification.type === "discord") { + this.sendDiscordNotification(networkResponse, notification.address); + } else if (notification.type === "slack") { + this.sendSlackNotification(networkResponse, notification.address); + } + // Handle other types of notifications here + } return true; } catch (error) { this.logger.warn({ From d3810db6cb3b5a93db01e985b48ac720b33cdcb5 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sat, 25 Jan 2025 16:13:16 -0800 Subject: [PATCH 03/31] Added telegram notifications --- Server/db/models/Notification.js | 2 +- Server/service/notificationService.js | 69 +++++++++++++++++++-------- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index 58433fbc0..4b24c8f49 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -8,7 +8,7 @@ const NotificationSchema = mongoose.Schema( }, type: { type: String, - enum: ["email", "sms", "discord", "slack"], + enum: ["email", "sms", "discord", "slack", "telegram"], }, address: { type: String, diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index f47a2a74c..b352dd729 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -57,6 +57,37 @@ class NotificationService { } } + async sendTelegramNotification(networkResponse, address) { + const { monitor, status } = networkResponse; + + const [botToken, chatId] = address.split('|').map(part => part?.trim()); + + if (!botToken || !chatId) { + return false; + } + + const message = { + chat_id: chatId, + text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`, + }; + + const url = `https://api.telegram.org/bot${botToken}/sendMessage`; + + try { + await axios.post(url, message, { + headers: { + 'Content-Type': 'application/json', + }, + }); + return true; + } catch (error) { + return false; + } + } + + + + /** * Sends an email notification for hardware infrastructure alerts * @@ -97,25 +128,25 @@ class NotificationService { } async handleStatusNotifications(networkResponse) { - try { - // If status hasn't changed, we're done - if (networkResponse.statusChanged === false) return false; - - // if prevStatus is undefined, monitor is resuming, we're done - if (networkResponse.prevStatus === undefined) return false; - - const notifications = await this.db.getNotificationsByMonitorId(networkResponse.monitorId); - - for (const notification of notifications) { - if (notification.type === "email") { - this.sendEmail(networkResponse, notification.address); - } else if (notification.type === "discord") { - this.sendDiscordNotification(networkResponse, notification.address); - } else if (notification.type === "slack") { - this.sendSlackNotification(networkResponse, notification.address); - } - // Handle other types of notifications here - } + try { + if (networkResponse.statusChanged === false) return false; + if (networkResponse.prevStatus === undefined) return false; + + const notifications = await this.db.getNotificationsByMonitorId(networkResponse.monitorId); + + for (const notification of notifications) { + if (notification.type === "email") { + this.sendEmail(networkResponse, notification.address); + } else if (notification.type === "discord") { + this.sendDiscordNotification(networkResponse, notification.address); + } else if (notification.type === "slack") { + this.sendSlackNotification(networkResponse, notification.address); + } else if (notification.type === "telegram") { + this.sendTelegramNotification(networkResponse, notification.address); + } + + // Handle other types of notifications here + } return true; } catch (error) { this.logger.warn({ From 84fe58b3617c8a467f4661fa621236f9be07d022 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 26 Jan 2025 14:11:36 -0800 Subject: [PATCH 04/31] Refactor webhook integrations functions. --- Server/service/notificationService.js | 90 +++++++++------------------ 1 file changed, 29 insertions(+), 61 deletions(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index b352dd729..a54afd506 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -17,75 +17,43 @@ class NotificationService { this.logger = logger; } - async sendDiscordNotification(networkResponse, webhookUrl) { + async sendWebhookNotification(networkResponse, address, platform) { const { monitor, status } = networkResponse; - const message = { - content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` - }; + let message; + let url = address; + + if (platform === 'slack') { + message = { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; + } else if (platform === 'discord') { + message = { content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; + } else if (platform === 'telegram') { + const [botToken, chatId] = address.split('|').map(part => part?.trim()); + if (!botToken || !chatId) { + console.error('Invalid Telegram address format'); + return false; + } + message = { + chat_id: chatId, + text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` + }; + url = `https://api.telegram.org/bot${botToken}/sendMessage`; + } try { - await axios.post(webhookUrl, message); + await axios.post(url, message, { + headers: { + 'Content-Type': 'application/json' + } + }); return true; } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "sendDiscordNotification", - stack: error.stack, - }); + + console.error(`Error sending ${platform} notification:`, error.toJSON()); + console.error(`URL: ${url}`); + console.error(`Message:`, message); return false; } } - - async sendSlackNotification(networkResponse, webhookUrl) { - const { monitor, status } = networkResponse; - const message = { - text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` - }; - - try { - await axios.post(webhookUrl, message); - return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "sendSlackNotification", - stack: error.stack, - }); - return false; - } - } - - async sendTelegramNotification(networkResponse, address) { - const { monitor, status } = networkResponse; - - const [botToken, chatId] = address.split('|').map(part => part?.trim()); - - if (!botToken || !chatId) { - return false; - } - - const message = { - chat_id: chatId, - text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`, - }; - - const url = `https://api.telegram.org/bot${botToken}/sendMessage`; - - try { - await axios.post(url, message, { - headers: { - 'Content-Type': 'application/json', - }, - }); - return true; - } catch (error) { - return false; - } - } - - /** From cc48f1d3aa08cf0899952f494be2937793df672b Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sat, 1 Feb 2025 23:06:35 -0800 Subject: [PATCH 05/31] Use the NetworkService for making network requests. --- Server/index.js | 14 ++++- Server/service/networkService.js | 46 ++++++++++++++ Server/service/notificationService.js | 90 +++++++++++++-------------- 3 files changed, 104 insertions(+), 46 deletions(-) diff --git a/Server/index.js b/Server/index.js index 7a4129bc9..a826ce64b 100644 --- a/Server/index.js +++ b/Server/index.js @@ -38,6 +38,10 @@ import QueueController from "./controllers/queueController.js"; import DistributedUptimeRoutes from "./routes/distributedUptimeRoute.js"; import DistributedUptimeController from "./controllers/distributedUptimeController.js"; +import NotificationRoutes from "./routes/notificationRoute.js"; // Add this line + +import NotificationController from "./controllers/notificationController.js"; + //JobQueue service and dependencies import JobQueue from "./service/jobQueue.js"; import { Queue, Worker } from "bullmq"; @@ -174,7 +178,7 @@ const startApp = async () => { ); const networkService = new NetworkService(axios, ping, logger, http, Docker, net); const statusService = new StatusService(db, logger); - const notificationService = new NotificationService(emailService, db, logger); + const notificationService = new NotificationService(emailService, db, logger, networkService); const jobQueue = new JobQueue( db, @@ -244,6 +248,10 @@ const startApp = async () => { ServiceRegistry.get(MongoDB.SERVICE_NAME) ); + const notificationController = new NotificationController( + ServiceRegistry.get(NotificationService.SERVICE_NAME) + ); + const distributedUptimeController = new DistributedUptimeController(); //Create routes @@ -260,6 +268,9 @@ const startApp = async () => { const distributedUptimeRoutes = new DistributedUptimeRoutes( distributedUptimeController ); + + const notificationRoutes = new NotificationRoutes(notificationController); + // Init job queue await jobQueue.initJobQueue(); // Middleware @@ -284,6 +295,7 @@ const startApp = async () => { app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter()); app.use("/api/v1/status-page", statusPageRoutes.getRouter()); + app.use("/api/v1/notifications", notificationRoutes.getRouter()); // Add this line app.use(handleErrors); }; diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 54edf39f9..604e2bbe6 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -336,6 +336,52 @@ class NetworkService { throw err; } + async requestWebhook(platform, url, message) { + try { + const { response, responseTime, error } = await this.timeRequest(() => + this.axios.post(url, message, { + headers: { + 'Content-Type': 'application/json' + } + }) + ); + + const webhookResponse = { + type: 'webhook', + responseTime, + payload: response?.data + }; + + if (error) { + webhookResponse.status = false; + webhookResponse.code = error.response?.status || this.NETWORK_ERROR; + webhookResponse.message = `Failed to send ${platform} notification`; + this.logger.warn({ + message: error.message, + service: this.SERVICE_NAME, + method: 'requestWebhook', + url, + platform, + error: error.message, + statusCode: error.response?.status, + responseData: error.response?.data, + requestPayload: message + }); + return webhookResponse; + } + + webhookResponse.status = true; + webhookResponse.code = response.status; + webhookResponse.message = `Successfully sent ${platform} notification`; + return webhookResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = 'requestWebhook'; + throw error; + } + } + + /** * Gets the status of a job based on its type and returns the appropriate response. * diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index a54afd506..bd65daf46 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -1,5 +1,6 @@ const SERVICE_NAME = "NotificationService"; -import axios from 'axios'; +import NetworkService from "./networkService.js"; + class NotificationService { static SERVICE_NAME = SERVICE_NAME; @@ -10,50 +11,53 @@ class NotificationService { * @param {Object} db - The database instance for storing notification data. * @param {Object} logger - The logger instance for logging activities. */ - constructor(emailService, db, logger) { + constructor(emailService, db, logger, networkService) { this.SERVICE_NAME = SERVICE_NAME; this.emailService = emailService; this.db = db; this.logger = logger; + this.networkService = networkService; } async sendWebhookNotification(networkResponse, address, platform) { - const { monitor, status } = networkResponse; - let message; - let url = address; - - if (platform === 'slack') { - message = { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; - } else if (platform === 'discord') { - message = { content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; - } else if (platform === 'telegram') { - const [botToken, chatId] = address.split('|').map(part => part?.trim()); - if (!botToken || !chatId) { - console.error('Invalid Telegram address format'); - return false; - } - message = { - chat_id: chatId, - text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` - }; - url = `https://api.telegram.org/bot${botToken}/sendMessage`; - } - - try { - await axios.post(url, message, { - headers: { - 'Content-Type': 'application/json' - } - }); - return true; - } catch (error) { - - console.error(`Error sending ${platform} notification:`, error.toJSON()); - console.error(`URL: ${url}`); - console.error(`Message:`, message); - return false; - } - } + const { monitor, status } = networkResponse; + let message; + let url = address; + + if (platform === 'slack') { + message = { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; + } else if (platform === 'discord') { + message = { content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; + } else if (platform === 'telegram') { + const [botToken, chatId] = address.split('|').map(part => part?.trim()); + if (!botToken || !chatId) { + return false; + } + message = { + chat_id: chatId, + text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` + }; + url = `https://api.telegram.org/bot${botToken}/sendMessage`; + } + + try { + const response = await this.networkService.requestWebhook(platform, url, message); + return response.status; + } catch (error) { + this.logger.error({ + message: `Error sending ${platform} notification`, + service: this.SERVICE_NAME, + method: 'sendWebhookNotification', + error: error.message, + stack: error.stack, + url, + platform, + requestPayload: message + }); + return false; + } + } + /** @@ -105,16 +109,12 @@ class NotificationService { for (const notification of notifications) { if (notification.type === "email") { this.sendEmail(networkResponse, notification.address); - } else if (notification.type === "discord") { - this.sendDiscordNotification(networkResponse, notification.address); - } else if (notification.type === "slack") { - this.sendSlackNotification(networkResponse, notification.address); - } else if (notification.type === "telegram") { - this.sendTelegramNotification(networkResponse, notification.address); - } + } else if (["discord", "slack", "telegram"].includes(notification.type)) { + this.sendWebhookNotification(networkResponse, notification.address, notification.type); // Handle other types of notifications here } + } return true; } catch (error) { this.logger.warn({ From 0bb7eae71e7127bd2e2a5d056d4bf838f6b7e956 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 2 Feb 2025 21:17:55 -0800 Subject: [PATCH 06/31] Got rid of imports. --- Server/service/notificationService.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index bd65daf46..374b1470e 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -1,6 +1,4 @@ const SERVICE_NAME = "NotificationService"; -import NetworkService from "./networkService.js"; - class NotificationService { static SERVICE_NAME = SERVICE_NAME; From d3fb41acfe7700e3641ec7120af9576e8118d6c7 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 2 Feb 2025 21:45:45 -0800 Subject: [PATCH 07/31] Refactor BASE URL. --- Server/service/notificationService.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 374b1470e..66a7ea889 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -1,4 +1,5 @@ const SERVICE_NAME = "NotificationService"; +const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot"; class NotificationService { static SERVICE_NAME = SERVICE_NAME; @@ -35,7 +36,7 @@ class NotificationService { chat_id: chatId, text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; - url = `https://api.telegram.org/bot${botToken}/sendMessage`; + url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; } try { From 8f9406261f9baa33ed427d5d2a6898eae174ee9c Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 2 Feb 2025 23:50:09 -0800 Subject: [PATCH 08/31] Store bot token and chat id in their own respective fields. --- Server/db/models/Notification.js | 10 ++++++++++ Server/service/notificationService.js | 10 ++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index 4b24c8f49..7a3d599ff 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -10,6 +10,15 @@ const NotificationSchema = mongoose.Schema( type: String, enum: ["email", "sms", "discord", "slack", "telegram"], }, + webhookUrl: { + type: String, + }, + botToken: { + type: String, + }, + chatId: { + type: String, + }, address: { type: String, }, @@ -76,4 +85,5 @@ NotificationSchema.pre("findOneAndUpdate", function (next) { } next(); }); + export default mongoose.model("Notification", NotificationSchema); diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 66a7ea889..05adbb3ff 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -18,17 +18,16 @@ class NotificationService { this.networkService = networkService; } - async sendWebhookNotification(networkResponse, address, platform) { + async sendWebhookNotification(networkResponse, address, platform, botToken, chatId) { const { monitor, status } = networkResponse; let message; let url = address; - + if (platform === 'slack') { message = { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; } else if (platform === 'discord') { message = { content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; } else if (platform === 'telegram') { - const [botToken, chatId] = address.split('|').map(part => part?.trim()); if (!botToken || !chatId) { return false; } @@ -38,7 +37,7 @@ class NotificationService { }; url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; } - + try { const response = await this.networkService.requestWebhook(platform, url, message); return response.status; @@ -56,9 +55,8 @@ class NotificationService { return false; } } - - + /** * Sends an email notification for hardware infrastructure alerts * From e44cbf376f989af82f30909f9cd7d2b13f094d2b Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 4 Feb 2025 14:50:16 -0800 Subject: [PATCH 09/31] Stop execution in the event of an unwanted platform. --- Server/service/notificationService.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 05adbb3ff..a8707dc4d 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -36,6 +36,14 @@ class NotificationService { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; + } else { + this.logger.warn({ + message: `Unsupported platform: ${platform}`, + service: this.SERVICE_NAME, + method: 'sendWebhookNotification', + platform + }); + return false; } try { From aa977ce34e67ad971aaf3d8d309e8b2fa4d637de Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 4 Feb 2025 15:00:20 -0800 Subject: [PATCH 10/31] Refactor out to a template for easier message maintenance. --- Server/service/notificationService.js | 48 ++++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index a8707dc4d..e31d6b473 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -18,34 +18,43 @@ class NotificationService { this.networkService = networkService; } + formatNotificationMessage(monitor, status, platform, chatId) { + const messageText = `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`; + + if (platform === 'telegram') { + return { chat_id: chatId, text: messageText }; + } + if (platform === 'slack') { + return { text: messageText }; + } + if (platform === 'discord') { + return { content: messageText }; + } + return null; + } + async sendWebhookNotification(networkResponse, address, platform, botToken, chatId) { const { monitor, status } = networkResponse; - let message; let url = address; - - if (platform === 'slack') { - message = { text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; - } else if (platform === 'discord') { - message = { content: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` }; - } else if (platform === 'telegram') { - if (!botToken || !chatId) { - return false; - } - message = { - chat_id: chatId, - text: `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}` - }; - url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; - } else { + + const message = this.formatNotificationMessage(monitor, status, platform, chatId); + if (!message) { this.logger.warn({ message: `Unsupported platform: ${platform}`, service: this.SERVICE_NAME, method: 'sendWebhookNotification', platform }); - return false; + return false; } - + + if (platform === 'telegram') { + if (!botToken || !chatId) { + return false; + } + url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; + } + try { const response = await this.networkService.requestWebhook(platform, url, message); return response.status; @@ -63,8 +72,7 @@ class NotificationService { return false; } } - - + /** * Sends an email notification for hardware infrastructure alerts * From 964a923265af8d65a92f091f6427153e47e655e4 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Tue, 4 Feb 2025 15:07:02 -0800 Subject: [PATCH 11/31] Returned comments. --- Server/service/notificationService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index e31d6b473..0f402626e 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -114,7 +114,9 @@ class NotificationService { async handleStatusNotifications(networkResponse) { try { + //If status hasn't changed, we're done if (networkResponse.statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done if (networkResponse.prevStatus === undefined) return false; const notifications = await this.db.getNotificationsByMonitorId(networkResponse.monitorId); From 64c5aa9bb095749b55672cbb82f1d0b1a7635164 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sat, 8 Feb 2025 17:07:14 -0800 Subject: [PATCH 12/31] Added notifcation controller and route. --- Server/controllers/notificationController.js | 65 ++++++++++++++++++++ Server/routes/notificationRoute.js | 19 ++++++ 2 files changed, 84 insertions(+) create mode 100644 Server/controllers/notificationController.js create mode 100644 Server/routes/notificationRoute.js diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js new file mode 100644 index 000000000..70b099feb --- /dev/null +++ b/Server/controllers/notificationController.js @@ -0,0 +1,65 @@ +import logger from '../utils/logger.js'; + +class NotificationController { + constructor(notificationService) { + this.notificationService = notificationService; + } + + async triggerNotification(req, res) { + const { monitorId, type, webhookUrl, botToken, chatId } = req.body; + + if (!monitorId || !type) { + return res.status(400).json({ + success: false, + msg: "monitorId and type are required" + }); + } + + try { + const networkResponse = { + monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" }, + status: false, + statusChanged: true, + prevStatus: true, + }; + + if (type === "telegram") { + if (!botToken || !chatId) { + return res.status(400).json({ + success: false, + msg: "botToken and chatId are required for Telegram notifications" + }); + } + await this.notificationService.sendWebhookNotification(networkResponse, null, type, botToken, chatId); + } else if (type === "discord" || type === "slack") { + if (!webhookUrl) { + return res.status(400).json({ + success: false, + msg: `webhookUrl is required for ${type} notifications` + }); + } + await this.notificationService.sendWebhookNotification(networkResponse, webhookUrl, type); + } else if (type === "email") { + if (!req.body.address) { + return res.status(400).json({ + success: false, + msg: "address is required for email notifications" + }); + } + await this.notificationService.sendEmail(networkResponse, req.body.address); + } + + res.json({ success: true, msg: "Notification sent successfully" }); + } catch (error) { + logger.error({ + message: error.message, + service: "NotificationController", + method: "triggerNotification", + stack: error.stack, + }); + res.status(500).json({ success: false, msg: "Failed to send notification" }); + } + } +} + +export default NotificationController; \ No newline at end of file diff --git a/Server/routes/notificationRoute.js b/Server/routes/notificationRoute.js new file mode 100644 index 000000000..6c7fc37d7 --- /dev/null +++ b/Server/routes/notificationRoute.js @@ -0,0 +1,19 @@ +import express from 'express'; + +class NotificationRoutes { + constructor(notificationController) { + this.notificationController = notificationController; + this.router = express.Router(); + this.initializeRoutes(); + } + + initializeRoutes() { + this.router.post('/trigger', this.notificationController.triggerNotification.bind(this.notificationController)); + } + + getRouter() { + return this.router; + } +} + +export default NotificationRoutes; \ No newline at end of file From d8379f7e882b2239437cb99b3815c2460fcb9041 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 9 Feb 2025 12:48:22 -0800 Subject: [PATCH 13/31] Configured a config object in the notification schema. --- Server/controllers/notificationController.js | 27 +++++++++++------- Server/db/models/Notification.js | 30 +++++++++++++------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index 70b099feb..22c6f473c 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -6,15 +6,15 @@ class NotificationController { } async triggerNotification(req, res) { - const { monitorId, type, webhookUrl, botToken, chatId } = req.body; - + const { monitorId, type, config } = req.body; + if (!monitorId || !type) { return res.status(400).json({ success: false, msg: "monitorId and type are required" }); } - + try { const networkResponse = { monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" }, @@ -22,33 +22,37 @@ class NotificationController { statusChanged: true, prevStatus: true, }; - + if (type === "telegram") { - if (!botToken || !chatId) { + if (!config?.botToken || !config?.chatId) { return res.status(400).json({ success: false, msg: "botToken and chatId are required for Telegram notifications" }); } - await this.notificationService.sendWebhookNotification(networkResponse, null, type, botToken, chatId); + await this.notificationService.sendWebhookNotification( + networkResponse, null, type, config.botToken, config.chatId + ); } else if (type === "discord" || type === "slack") { - if (!webhookUrl) { + if (!config?.webhookUrl) { return res.status(400).json({ success: false, msg: `webhookUrl is required for ${type} notifications` }); } - await this.notificationService.sendWebhookNotification(networkResponse, webhookUrl, type); + await this.notificationService.sendWebhookNotification( + networkResponse, config.webhookUrl, type + ); } else if (type === "email") { - if (!req.body.address) { + if (!config?.address) { return res.status(400).json({ success: false, msg: "address is required for email notifications" }); } - await this.notificationService.sendEmail(networkResponse, req.body.address); + await this.notificationService.sendEmail(networkResponse, config.address); } - + res.json({ success: true, msg: "Notification sent successfully" }); } catch (error) { logger.error({ @@ -60,6 +64,7 @@ class NotificationController { res.status(500).json({ success: false, msg: "Failed to send notification" }); } } + } export default NotificationController; \ No newline at end of file diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index 7a3d599ff..aebbdd2fb 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -8,17 +8,14 @@ const NotificationSchema = mongoose.Schema( }, type: { type: String, - enum: ["email", "sms", "discord", "slack", "telegram"], + enum: ["email", "sms"], }, - webhookUrl: { - type: String, - }, - botToken: { - type: String, - }, - chatId: { - type: String, - }, + config: { + webhookUrl: { type: String }, // For Discord & Slack + botToken: { type: String }, // For Telegram + chatId: { type: String }, // For Telegram + }, + address: { type: String, }, @@ -59,6 +56,19 @@ const NotificationSchema = mongoose.Schema( } ); +NotificationSchema.pre("save", function (next) { + if (this.type === "telegram" && (!this.config.botToken || !this.config.chatId)) { + return next(new Error("botToken and chatId are required for Telegram notifications")); + } + if ((this.type === "discord" || this.type === "slack") && !this.config.webhookUrl) { + return next(new Error(`webhookUrl is required for ${this.type} notifications`)); + } + if (this.type === "email" && !this.config.address) { + return next(new Error("address is required for email notifications")); + } + next(); + }); + NotificationSchema.pre("save", function (next) { if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { this.cpuAlertThreshold = this.alertThreshold; From f46b0768fcf706bd5b8ea3d50ab51ad683256a8e Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 9 Feb 2025 13:58:14 -0800 Subject: [PATCH 14/31] Refactored notification schema config object. --- Server/controllers/notificationController.js | 45 ++++++++++---------- Server/db/models/Notification.js | 23 +++------- Server/service/notificationService.js | 19 +++++---- 3 files changed, 37 insertions(+), 50 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index 22c6f473c..dadf86a58 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -23,34 +23,34 @@ class NotificationController { prevStatus: true, }; - if (type === "telegram") { - if (!config?.botToken || !config?.chatId) { + if (type === "webhook") { + if (!config?.type) { return res.status(400).json({ success: false, - msg: "botToken and chatId are required for Telegram notifications" + msg: "webhook type is required in config" }); } + + if (config.type === "telegram") { + if (!config.botToken || !config.chatId) { + return res.status(400).json({ + success: false, + msg: "botToken and chatId are required for Telegram notifications" + }); + } + } else if (["discord", "slack"].includes(config.type)) { + if (!config.webhookUrl) { + return res.status(400).json({ + success: false, + msg: `webhookUrl is required for ${config.type} notifications` + }); + } + } + await this.notificationService.sendWebhookNotification( - networkResponse, null, type, config.botToken, config.chatId + networkResponse, + config ); - } else if (type === "discord" || type === "slack") { - if (!config?.webhookUrl) { - return res.status(400).json({ - success: false, - msg: `webhookUrl is required for ${type} notifications` - }); - } - await this.notificationService.sendWebhookNotification( - networkResponse, config.webhookUrl, type - ); - } else if (type === "email") { - if (!config?.address) { - return res.status(400).json({ - success: false, - msg: "address is required for email notifications" - }); - } - await this.notificationService.sendEmail(networkResponse, config.address); } res.json({ success: true, msg: "Notification sent successfully" }); @@ -64,7 +64,6 @@ class NotificationController { res.status(500).json({ success: false, msg: "Failed to send notification" }); } } - } export default NotificationController; \ No newline at end of file diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index aebbdd2fb..7d684bbc1 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -8,14 +8,14 @@ const NotificationSchema = mongoose.Schema( }, type: { type: String, - enum: ["email", "sms"], + enum: ["email", "sms", "webhook"], }, config: { - webhookUrl: { type: String }, // For Discord & Slack - botToken: { type: String }, // For Telegram - chatId: { type: String }, // For Telegram + type: String, + webhookUrl: String, + botToken: String, + chatId: String }, - address: { type: String, }, @@ -56,19 +56,6 @@ const NotificationSchema = mongoose.Schema( } ); -NotificationSchema.pre("save", function (next) { - if (this.type === "telegram" && (!this.config.botToken || !this.config.chatId)) { - return next(new Error("botToken and chatId are required for Telegram notifications")); - } - if ((this.type === "discord" || this.type === "slack") && !this.config.webhookUrl) { - return next(new Error(`webhookUrl is required for ${this.type} notifications`)); - } - if (this.type === "email" && !this.config.address) { - return next(new Error("address is required for email notifications")); - } - next(); - }); - NotificationSchema.pre("save", function (next) { if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { this.cpuAlertThreshold = this.alertThreshold; diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 0f402626e..ebd27c166 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -33,22 +33,23 @@ class NotificationService { return null; } - async sendWebhookNotification(networkResponse, address, platform, botToken, chatId) { + async sendWebhookNotification(networkResponse, config) { const { monitor, status } = networkResponse; - let url = address; + const { type, webhookUrl, botToken, chatId } = config; + let url = webhookUrl; - const message = this.formatNotificationMessage(monitor, status, platform, chatId); + const message = this.formatNotificationMessage(monitor, status, type, chatId); if (!message) { this.logger.warn({ - message: `Unsupported platform: ${platform}`, + message: `Unsupported webhook type: ${type}`, service: this.SERVICE_NAME, method: 'sendWebhookNotification', - platform + type }); return false; } - if (platform === 'telegram') { + if (type === 'telegram') { if (!botToken || !chatId) { return false; } @@ -56,17 +57,17 @@ class NotificationService { } try { - const response = await this.networkService.requestWebhook(platform, url, message); + const response = await this.networkService.requestWebhook(type, url, message); return response.status; } catch (error) { this.logger.error({ - message: `Error sending ${platform} notification`, + message: `Error sending ${type} notification`, service: this.SERVICE_NAME, method: 'sendWebhookNotification', error: error.message, stack: error.stack, url, - platform, + type, requestPayload: message }); return false; From ef41712544a43b7915142992bdc720b2cd8d346a Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 10 Feb 2025 18:46:39 -0800 Subject: [PATCH 15/31] Got rid of falsey value. --- Server/service/notificationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index ebd27c166..59698f0c5 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -39,7 +39,7 @@ class NotificationService { let url = webhookUrl; const message = this.formatNotificationMessage(monitor, status, type, chatId); - if (!message) { + if (message === null) { this.logger.warn({ message: `Unsupported webhook type: ${type}`, service: this.SERVICE_NAME, From fa50706541e6c49512a2153a3379a720fcb57ff4 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 10 Feb 2025 18:52:06 -0800 Subject: [PATCH 16/31] Simplified network response. --- Server/service/notificationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 59698f0c5..1e0202d39 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -125,7 +125,7 @@ class NotificationService { for (const notification of notifications) { if (notification.type === "email") { this.sendEmail(networkResponse, notification.address); - } else if (["discord", "slack", "telegram"].includes(notification.type)) { + } else if (notification.type === "webhook") { this.sendWebhookNotification(networkResponse, notification.address, notification.type); // Handle other types of notifications here From 67da574ea1d3ad06b994ec4ab47d36270855d993 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 10 Feb 2025 19:44:44 -0800 Subject: [PATCH 17/31] Secured notifications route. --- Server/index.js | 4 ++-- Server/routes/notificationRoute.js | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Server/index.js b/Server/index.js index bc7be395d..8fb58481c 100644 --- a/Server/index.js +++ b/Server/index.js @@ -38,7 +38,7 @@ import QueueController from "./controllers/queueController.js"; import DistributedUptimeRoutes from "./routes/distributedUptimeRoute.js"; import DistributedUptimeController from "./controllers/distributedUptimeController.js"; -import NotificationRoutes from "./routes/notificationRoute.js"; // Add this line +import NotificationRoutes from "./routes/notificationRoute.js"; import NotificationController from "./controllers/notificationController.js"; @@ -299,7 +299,7 @@ const startApp = async () => { app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter()); app.use("/api/v1/status-page", statusPageRoutes.getRouter()); - app.use("/api/v1/notifications", notificationRoutes.getRouter()); // Add this line + app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); app.use(handleErrors); }; diff --git a/Server/routes/notificationRoute.js b/Server/routes/notificationRoute.js index 6c7fc37d7..d1c420af6 100644 --- a/Server/routes/notificationRoute.js +++ b/Server/routes/notificationRoute.js @@ -1,4 +1,5 @@ import express from 'express'; +import { verifyJWT } from '../middleware/verifyJWT.js'; class NotificationRoutes { constructor(notificationController) { @@ -8,7 +9,7 @@ class NotificationRoutes { } initializeRoutes() { - this.router.post('/trigger', this.notificationController.triggerNotification.bind(this.notificationController)); + this.router.post('/trigger', verifyJWT, this.notificationController.triggerNotification.bind(this.notificationController)); } getRouter() { @@ -16,4 +17,4 @@ class NotificationRoutes { } } -export default NotificationRoutes; \ No newline at end of file +export default NotificationRoutes; From 012483d14f472401a8316e3c01b6babab47d31fa Mon Sep 17 00:00:00 2001 From: Skorpios Date: Wed, 12 Feb 2025 18:54:34 -0800 Subject: [PATCH 18/31] Automated validation via joi. --- Server/controllers/notificationController.js | 36 +----------- Server/routes/notificationRoute.js | 32 ++++++++++- Server/validation/joi.js | 59 +++++++++++++++++++- 3 files changed, 90 insertions(+), 37 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index dadf86a58..6f28579ff 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -6,53 +6,21 @@ class NotificationController { } async triggerNotification(req, res) { - const { monitorId, type, config } = req.body; - - if (!monitorId || !type) { - return res.status(400).json({ - success: false, - msg: "monitorId and type are required" - }); - } - try { + const { monitorId, type, config } = req.body; + const networkResponse = { monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" }, status: false, statusChanged: true, prevStatus: true, }; - if (type === "webhook") { - if (!config?.type) { - return res.status(400).json({ - success: false, - msg: "webhook type is required in config" - }); - } - - if (config.type === "telegram") { - if (!config.botToken || !config.chatId) { - return res.status(400).json({ - success: false, - msg: "botToken and chatId are required for Telegram notifications" - }); - } - } else if (["discord", "slack"].includes(config.type)) { - if (!config.webhookUrl) { - return res.status(400).json({ - success: false, - msg: `webhookUrl is required for ${config.type} notifications` - }); - } - } - await this.notificationService.sendWebhookNotification( networkResponse, config ); } - res.json({ success: true, msg: "Notification sent successfully" }); } catch (error) { logger.error({ diff --git a/Server/routes/notificationRoute.js b/Server/routes/notificationRoute.js index d1c420af6..4f8ae8539 100644 --- a/Server/routes/notificationRoute.js +++ b/Server/routes/notificationRoute.js @@ -1,5 +1,6 @@ import express from 'express'; import { verifyJWT } from '../middleware/verifyJWT.js'; +import { triggerNotificationBodyValidation } from '../validation/joi.js'; class NotificationRoutes { constructor(notificationController) { @@ -8,8 +9,35 @@ class NotificationRoutes { this.initializeRoutes(); } + validateRequest(schema) { + return (req, res, next) => { + const { error } = schema.validate(req.body, { + abortEarly: false, + stripUnknown: true + }); + + if (error) { + const errorMessage = error.details + .map(detail => detail.message) + .join(', '); + + return res.status(400).json({ + success: false, + msg: errorMessage + }); + } + + next(); + }; + } + initializeRoutes() { - this.router.post('/trigger', verifyJWT, this.notificationController.triggerNotification.bind(this.notificationController)); + this.router.post( + '/trigger', + verifyJWT, + this.validateRequest(triggerNotificationBodyValidation), + this.notificationController.triggerNotification.bind(this.notificationController) + ); } getRouter() { @@ -17,4 +45,4 @@ class NotificationRoutes { } } -export default NotificationRoutes; +export default NotificationRoutes; \ No newline at end of file diff --git a/Server/validation/joi.js b/Server/validation/joi.js index b60022fc5..a03b03055 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -475,6 +475,59 @@ const imageValidation = joi "any.required": "Image file is required", }); + const telegramWebhookConfigValidation = joi.object({ + type: joi.string().valid('telegram').required(), + botToken: joi.string().required().messages({ + 'string.empty': 'Telegram bot token is required', + 'any.required': 'Telegram bot token is required' + }), + chatId: joi.string().required().messages({ + 'string.empty': 'Telegram chat ID is required', + 'any.required': 'Telegram chat ID is required' + }) + }); + + const discordWebhookConfigValidation = joi.object({ + type: joi.string().valid('discord').required(), + webhookUrl: joi.string().uri().required().messages({ + 'string.empty': 'Discord webhook URL is required', + 'string.uri': 'Discord webhook URL must be a valid URL', + 'any.required': 'Discord webhook URL is required' + }) + }); + + const slackWebhookConfigValidation = joi.object({ + type: joi.string().valid('slack').required(), + webhookUrl: joi.string().uri().required().messages({ + 'string.empty': 'Slack webhook URL is required', + 'string.uri': 'Slack webhook URL must be a valid URL', + 'any.required': 'Slack webhook URL is required' + }) + }); + + const triggerNotificationBodyValidation = joi.object({ + monitorId: joi.string().required().messages({ + 'string.empty': 'Monitor ID is required', + 'any.required': 'Monitor ID is required' + }), + type: joi.string().valid('webhook').required().messages({ + 'string.empty': 'Notification type is required', + 'any.required': 'Notification type is required', + 'any.only': 'Notification type must be webhook' + }), + config: joi.alternatives() + .conditional('type', { + is: 'webhook', + then: joi.alternatives().try( + telegramWebhookConfigValidation, + discordWebhookConfigValidation, + slackWebhookConfigValidation + ).required().messages({ + 'any.required': 'Webhook configuration is required' + }) + }) + }); + export { roleValidatior, loginValidation, @@ -533,5 +586,9 @@ export { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, - imageValidation, + imageValidation, + triggerNotificationBodyValidation, + telegramWebhookConfigValidation, + discordWebhookConfigValidation, + slackWebhookConfigValidation }; From 8007daa66bfcc1b3ddd5fa7fab0fa432b9cbeb33 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Wed, 12 Feb 2025 19:16:20 -0800 Subject: [PATCH 19/31] Used response handling middleware for the response format. --- Server/controllers/notificationController.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index 6f28579ff..e202c5274 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -15,13 +15,18 @@ class NotificationController { statusChanged: true, prevStatus: true, }; + if (type === "webhook") { await this.notificationService.sendWebhookNotification( networkResponse, config ); } - res.json({ success: true, msg: "Notification sent successfully" }); + + return res.success({ + msg: "Notification sent successfully" + }); + } catch (error) { logger.error({ message: error.message, @@ -29,7 +34,10 @@ class NotificationController { method: "triggerNotification", stack: error.stack, }); - res.status(500).json({ success: false, msg: "Failed to send notification" }); + + return res.error({ + msg: "Failed to send notification" + }); } } } From 70ffe1b6614396c944c05f7d27ca30d8d9e737e4 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Wed, 12 Feb 2025 21:17:31 -0800 Subject: [PATCH 20/31] Use middleware for handling errors. --- Server/controllers/notificationController.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index e202c5274..6a19c3a1b 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -1,11 +1,10 @@ -import logger from '../utils/logger.js'; class NotificationController { constructor(notificationService) { this.notificationService = notificationService; } - async triggerNotification(req, res) { + async triggerNotification(req, res, next) { try { const { monitorId, type, config } = req.body; @@ -28,16 +27,9 @@ class NotificationController { }); } catch (error) { - logger.error({ - message: error.message, - service: "NotificationController", - method: "triggerNotification", - stack: error.stack, - }); - - return res.error({ - msg: "Failed to send notification" - }); + error.service = "NotificationController"; + error.method = "triggerNotification"; + next(error); } } } From 12d9f154bf09e21c62e320b350017bb24e93c1ac Mon Sep 17 00:00:00 2001 From: Skorpios Date: Wed, 12 Feb 2025 21:22:44 -0800 Subject: [PATCH 21/31] Got rid context bind in the route. --- Server/controllers/notificationController.js | 1 + Server/routes/notificationRoute.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index 6a19c3a1b..362b5b381 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -2,6 +2,7 @@ class NotificationController { constructor(notificationService) { this.notificationService = notificationService; + this.triggerNotification = this.triggerNotification.bind(this); } async triggerNotification(req, res, next) { diff --git a/Server/routes/notificationRoute.js b/Server/routes/notificationRoute.js index 4f8ae8539..1b38d91c8 100644 --- a/Server/routes/notificationRoute.js +++ b/Server/routes/notificationRoute.js @@ -36,7 +36,7 @@ class NotificationRoutes { '/trigger', verifyJWT, this.validateRequest(triggerNotificationBodyValidation), - this.notificationController.triggerNotification.bind(this.notificationController) + this.notificationController.triggerNotification ); } From 8409571d57b4824a60a9edf069ec6396cd5e5b94 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Feb 2025 19:32:52 -0800 Subject: [PATCH 22/31] Defined new schema for the config object. --- Server/controllers/notificationController.js | 19 +++++---- Server/db/models/Notification.js | 15 ++++--- Server/service/notificationService.js | 28 ++++++------- Server/validation/joi.js | 44 +++++++++++++------- 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index 362b5b381..cdb49b90d 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -1,13 +1,12 @@ - class NotificationController { constructor(notificationService) { this.notificationService = notificationService; this.triggerNotification = this.triggerNotification.bind(this); } - async triggerNotification(req, res, next) { + async triggerNotification(req, res, next) { try { - const { monitorId, type, config } = req.body; + const { monitorId, type, platform, config } = req.body; const networkResponse = { monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" }, @@ -15,18 +14,24 @@ class NotificationController { statusChanged: true, prevStatus: true, }; - + if (type === "webhook") { + const notification = { + type, + platform, + config + }; + await this.notificationService.sendWebhookNotification( networkResponse, - config + notification ); } - + return res.success({ msg: "Notification sent successfully" }); - + } catch (error) { error.service = "NotificationController"; error.method = "triggerNotification"; diff --git a/Server/db/models/Notification.js b/Server/db/models/Notification.js index 7d684bbc1..53d9eb4a2 100644 --- a/Server/db/models/Notification.js +++ b/Server/db/models/Notification.js @@ -1,4 +1,11 @@ import mongoose from "mongoose"; + +const configSchema = mongoose.Schema({ + webhookUrl: { type: String }, + botToken: { type: String }, + chatId: { type: String } +}, { _id: false }); + const NotificationSchema = mongoose.Schema( { monitorId: { @@ -11,11 +18,9 @@ const NotificationSchema = mongoose.Schema( enum: ["email", "sms", "webhook"], }, config: { - type: String, - webhookUrl: String, - botToken: String, - chatId: String - }, + type: configSchema, + default: () => ({}) + }, address: { type: String, }, diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 1e0202d39..b182e273a 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -33,23 +33,24 @@ class NotificationService { return null; } - async sendWebhookNotification(networkResponse, config) { + async sendWebhookNotification(networkResponse, notification) { const { monitor, status } = networkResponse; - const { type, webhookUrl, botToken, chatId } = config; + const { platform } = notification; // Use platform instead of type for webhook formatting + const { webhookUrl, botToken, chatId } = notification.config; let url = webhookUrl; - const message = this.formatNotificationMessage(monitor, status, type, chatId); + const message = this.formatNotificationMessage(monitor, status, platform, chatId); if (message === null) { this.logger.warn({ - message: `Unsupported webhook type: ${type}`, + message: `Unsupported webhook platform: ${platform}`, service: this.SERVICE_NAME, method: 'sendWebhookNotification', - type + platform }); return false; } - if (type === 'telegram') { + if (platform === 'telegram') { if (!botToken || !chatId) { return false; } @@ -57,17 +58,17 @@ class NotificationService { } try { - const response = await this.networkService.requestWebhook(type, url, message); + const response = await this.networkService.requestWebhook(platform, url, message); return response.status; } catch (error) { this.logger.error({ - message: `Error sending ${type} notification`, + message: `Error sending ${platform} notification`, service: this.SERVICE_NAME, method: 'sendWebhookNotification', error: error.message, stack: error.stack, url, - type, + platform, requestPayload: message }); return false; @@ -115,7 +116,7 @@ class NotificationService { async handleStatusNotifications(networkResponse) { try { - //If status hasn't changed, we're done + // If status hasn't changed, we're done if (networkResponse.statusChanged === false) return false; // if prevStatus is undefined, monitor is resuming, we're done if (networkResponse.prevStatus === undefined) return false; @@ -124,13 +125,12 @@ class NotificationService { for (const notification of notifications) { if (notification.type === "email") { - this.sendEmail(networkResponse, notification.address); + await this.sendEmail(networkResponse, notification.address); } else if (notification.type === "webhook") { - this.sendWebhookNotification(networkResponse, notification.address, notification.type); - + await this.sendWebhookNotification(networkResponse, notification); + } // Handle other types of notifications here } - } return true; } catch (error) { this.logger.warn({ diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 565b62d26..1b285b0a5 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -476,7 +476,7 @@ const imageValidation = joi }); const telegramWebhookConfigValidation = joi.object({ - type: joi.string().valid('telegram').required(), + webhookUrl: joi.string().uri().optional(), botToken: joi.string().required().messages({ 'string.empty': 'Telegram bot token is required', 'any.required': 'Telegram bot token is required' @@ -488,21 +488,23 @@ const imageValidation = joi }); const discordWebhookConfigValidation = joi.object({ - type: joi.string().valid('discord').required(), webhookUrl: joi.string().uri().required().messages({ 'string.empty': 'Discord webhook URL is required', 'string.uri': 'Discord webhook URL must be a valid URL', 'any.required': 'Discord webhook URL is required' - }) + }), + botToken: joi.string().optional(), + chatId: joi.string().optional() }); const slackWebhookConfigValidation = joi.object({ - type: joi.string().valid('slack').required(), webhookUrl: joi.string().uri().required().messages({ 'string.empty': 'Slack webhook URL is required', 'string.uri': 'Slack webhook URL must be a valid URL', 'any.required': 'Slack webhook URL is required' - }) + }), + botToken: joi.string().optional(), + chatId: joi.string().optional() }); const triggerNotificationBodyValidation = joi.object({ @@ -515,16 +517,30 @@ const imageValidation = joi 'any.required': 'Notification type is required', 'any.only': 'Notification type must be webhook' }), + + platform: joi.string().valid('telegram', 'discord', 'slack').required().messages({ + 'string.empty': 'Platform type is required', + 'any.required': 'Platform type is required', + 'any.only': 'Platform must be telegram, discord, or slack' + }), config: joi.alternatives() - .conditional('type', { - is: 'webhook', - then: joi.alternatives().try( - telegramWebhookConfigValidation, - discordWebhookConfigValidation, - slackWebhookConfigValidation - ).required().messages({ - 'any.required': 'Webhook configuration is required' - }) + .conditional('platform', [ + { + is: 'telegram', + then: telegramWebhookConfigValidation + }, + { + is: 'discord', + then: discordWebhookConfigValidation + }, + { + is: 'slack', + then: slackWebhookConfigValidation + } + ]) + .required() + .messages({ + 'any.required': 'Webhook configuration is required' }) }); From 55c1a1c23312dd697e9846d192f601134821422f Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Feb 2025 20:21:44 -0800 Subject: [PATCH 23/31] Moved validation to the controller. --- Server/controllers/notificationController.js | 34 +++++++++++++++----- Server/routes/notificationRoute.js | 24 -------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index cdb49b90d..a65206cbe 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -1,3 +1,13 @@ +import { + triggerNotificationBodyValidation, + telegramWebhookConfigValidation, + discordWebhookConfigValidation, + slackWebhookConfigValidation +} from '../validation/joi.js'; +import { handleError, handleValidationError } from './controllerUtils.js'; + +const SERVICE_NAME = "NotificationController"; + class NotificationController { constructor(notificationService) { this.notificationService = notificationService; @@ -5,20 +15,30 @@ class NotificationController { } async triggerNotification(req, res, next) { + try { + await triggerNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + stripUnknown: true + }); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + return; + } + try { const { monitorId, type, platform, config } = req.body; - + const networkResponse = { monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" }, status: false, statusChanged: true, prevStatus: true, }; - + if (type === "webhook") { const notification = { type, - platform, + platform, config }; @@ -27,15 +47,13 @@ class NotificationController { notification ); } - + return res.success({ msg: "Notification sent successfully" }); - + } catch (error) { - error.service = "NotificationController"; - error.method = "triggerNotification"; - next(error); + next(handleError(error, SERVICE_NAME, "triggerNotification")); } } } diff --git a/Server/routes/notificationRoute.js b/Server/routes/notificationRoute.js index 1b38d91c8..6e13a53d1 100644 --- a/Server/routes/notificationRoute.js +++ b/Server/routes/notificationRoute.js @@ -1,6 +1,5 @@ import express from 'express'; import { verifyJWT } from '../middleware/verifyJWT.js'; -import { triggerNotificationBodyValidation } from '../validation/joi.js'; class NotificationRoutes { constructor(notificationController) { @@ -9,33 +8,10 @@ class NotificationRoutes { this.initializeRoutes(); } - validateRequest(schema) { - return (req, res, next) => { - const { error } = schema.validate(req.body, { - abortEarly: false, - stripUnknown: true - }); - - if (error) { - const errorMessage = error.details - .map(detail => detail.message) - .join(', '); - - return res.status(400).json({ - success: false, - msg: errorMessage - }); - } - - next(); - }; - } - initializeRoutes() { this.router.post( '/trigger', verifyJWT, - this.validateRequest(triggerNotificationBodyValidation), this.notificationController.triggerNotification ); } From ff2b3b4e10d8b2d659a4a969d1a72c88e9b9bbb4 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Feb 2025 20:28:34 -0800 Subject: [PATCH 24/31] Removed timing request for the webhook. --- Server/service/networkService.js | 64 +++++++++++++++----------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 9d0342d27..7b49184b6 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -437,46 +437,40 @@ class NetworkService { async requestWebhook(platform, url, message) { try { - const { response, responseTime, error } = await this.timeRequest(() => - this.axios.post(url, message, { - headers: { - 'Content-Type': 'application/json' - } - }) - ); + const response = await this.axios.post(url, message, { + headers: { + 'Content-Type': 'application/json' + } + }); - const webhookResponse = { + return { type: 'webhook', - responseTime, - payload: response?.data + status: true, + code: response.status, + message: `Successfully sent ${platform} notification`, + payload: response.data }; - if (error) { - webhookResponse.status = false; - webhookResponse.code = error.response?.status || this.NETWORK_ERROR; - webhookResponse.message = `Failed to send ${platform} notification`; - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: 'requestWebhook', - url, - platform, - error: error.message, - statusCode: error.response?.status, - responseData: error.response?.data, - requestPayload: message - }); - return webhookResponse; - } - - webhookResponse.status = true; - webhookResponse.code = response.status; - webhookResponse.message = `Successfully sent ${platform} notification`; - return webhookResponse; } catch (error) { - error.service = this.SERVICE_NAME; - error.method = 'requestWebhook'; - throw error; + this.logger.warn({ + message: error.message, + service: this.SERVICE_NAME, + method: 'requestWebhook', + url, + platform, + error: error.message, + statusCode: error.response?.status, + responseData: error.response?.data, + requestPayload: message + }); + + return { + type: 'webhook', + status: false, + code: error.response?.status || this.NETWORK_ERROR, + message: `Failed to send ${platform} notification`, + payload: error.response?.data + }; } } From 6152679dfa92cac3c0b42b7fb69ac44afdcafcbc Mon Sep 17 00:00:00 2001 From: Skorpios Date: Sun, 16 Feb 2025 20:33:47 -0800 Subject: [PATCH 25/31] Update Docs. --- Server/service/notificationService.js | 30 ++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index b182e273a..d19e1fe32 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -9,6 +9,7 @@ class NotificationService { * @param {Object} emailService - The email service used for sending notifications. * @param {Object} db - The database instance for storing notification data. * @param {Object} logger - The logger instance for logging activities. + * @param {Object} networkService - The network service for sending webhook notifications. */ constructor(emailService, db, logger, networkService) { this.SERVICE_NAME = SERVICE_NAME; @@ -18,6 +19,18 @@ class NotificationService { this.networkService = networkService; } + /** + * Formats a notification message based on the monitor status and platform. + * + * @param {Object} monitor - The monitor object. + * @param {string} monitor.name - The name of the monitor. + * @param {string} monitor.url - The URL of the monitor. + * @param {boolean} status - The current status of the monitor (true for up, false for down). + * @param {string} platform - The notification platform (e.g., "telegram", "slack", "discord"). + * @param {string} [chatId] - The chat ID for platforms that require it (e.g., Telegram). + * @returns {Object|null} The formatted message object for the specified platform, or null if the platform is unsupported. + */ + formatNotificationMessage(monitor, status, platform, chatId) { const messageText = `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`; @@ -33,9 +46,24 @@ class NotificationService { return null; } + /** + * Sends a webhook notification to a specified platform. + * + * @param {Object} networkResponse - The response object from the network. + * @param {Object} networkResponse.monitor - The monitor object. + * @param {boolean} networkResponse.status - The monitor's status (true for up, false for down). + * @param {Object} notification - The notification settings. + * @param {string} notification.platform - The target platform ("telegram", "slack", "discord"). + * @param {Object} notification.config - The configuration object for the webhook. + * @param {string} notification.config.webhookUrl - The webhook URL for the platform. + * @param {string} [notification.config.botToken] - The bot token for Telegram notifications. + * @param {string} [notification.config.chatId] - The chat ID for Telegram notifications. + * @returns {Promise} A promise that resolves to true if the notification was sent successfully, otherwise false. + */ + async sendWebhookNotification(networkResponse, notification) { const { monitor, status } = networkResponse; - const { platform } = notification; // Use platform instead of type for webhook formatting + const { platform } = notification; const { webhookUrl, botToken, chatId } = notification.config; let url = webhookUrl; From f22b67ad0a211de3cb98984e25a9c56b417703ef Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Feb 2025 09:12:11 -0800 Subject: [PATCH 26/31] Use the localization service for user facing strings. --- Server/controllers/notificationController.js | 3 +- Server/index.js | 5 +- Server/service/notificationService.js | 113 ++++++++++--------- Server/service/stringService.js | 30 +++++ 4 files changed, 94 insertions(+), 57 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index a65206cbe..7681d7c2a 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -9,8 +9,9 @@ import { handleError, handleValidationError } from './controllerUtils.js'; const SERVICE_NAME = "NotificationController"; class NotificationController { - constructor(notificationService) { + constructor(notificationService, stringService) { this.notificationService = notificationService; + this.stringService = stringService; this.triggerNotification = this.triggerNotification.bind(this); } diff --git a/Server/index.js b/Server/index.js index 090498de2..0733e9b78 100644 --- a/Server/index.js +++ b/Server/index.js @@ -187,7 +187,7 @@ const startApp = async () => { logger ); const statusService = new StatusService(db, logger); - const notificationService = new NotificationService(emailService, db, logger, networkService); + const notificationService = new NotificationService(emailService, db, logger, networkService, stringService); const jobQueue = new JobQueue( @@ -274,7 +274,8 @@ const startApp = async () => { ); const notificationController = new NotificationController( - ServiceRegistry.get(NotificationService.SERVICE_NAME) + ServiceRegistry.get(NotificationService.SERVICE_NAME), + ServiceRegistry.get(StringService.SERVICE_NAME) ); const distributedUptimeController = new DistributedUptimeController( diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index d19e1fe32..be8432336 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -11,12 +11,13 @@ class NotificationService { * @param {Object} logger - The logger instance for logging activities. * @param {Object} networkService - The network service for sending webhook notifications. */ - constructor(emailService, db, logger, networkService) { + constructor(emailService, db, logger, networkService, stringService) { this.SERVICE_NAME = SERVICE_NAME; this.emailService = emailService; this.db = db; this.logger = logger; this.networkService = networkService; + this.stringService = stringService; } /** @@ -32,19 +33,23 @@ class NotificationService { */ formatNotificationMessage(monitor, status, platform, chatId) { - const messageText = `Monitor ${monitor.name} is ${status ? "up" : "down"}. URL: ${monitor.url}`; - - if (platform === 'telegram') { - return { chat_id: chatId, text: messageText }; - } - if (platform === 'slack') { - return { text: messageText }; - } - if (platform === 'discord') { - return { content: messageText }; - } - return null; - } + const messageText = this.stringService.getMonitorStatus( + monitor.name, + status, + monitor.url + ); + + if (platform === 'telegram') { + return { chat_id: chatId, text: messageText }; + } + if (platform === 'slack') { + return { text: messageText }; + } + if (platform === 'discord') { + return { content: messageText }; + } + return null; + } /** * Sends a webhook notification to a specified platform. @@ -62,46 +67,46 @@ class NotificationService { */ async sendWebhookNotification(networkResponse, notification) { - const { monitor, status } = networkResponse; - const { platform } = notification; - const { webhookUrl, botToken, chatId } = notification.config; - let url = webhookUrl; - - const message = this.formatNotificationMessage(monitor, status, platform, chatId); - if (message === null) { - this.logger.warn({ - message: `Unsupported webhook platform: ${platform}`, - service: this.SERVICE_NAME, - method: 'sendWebhookNotification', - platform - }); - return false; - } - - if (platform === 'telegram') { - if (!botToken || !chatId) { - return false; - } - url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; - } - - try { - const response = await this.networkService.requestWebhook(platform, url, message); - return response.status; - } catch (error) { - this.logger.error({ - message: `Error sending ${platform} notification`, - service: this.SERVICE_NAME, - method: 'sendWebhookNotification', - error: error.message, - stack: error.stack, - url, - platform, - requestPayload: message - }); - return false; - } - } + const { monitor, status } = networkResponse; + const { platform } = notification; + const { webhookUrl, botToken, chatId } = notification.config; + let url = webhookUrl; + + const message = this.formatNotificationMessage(monitor, status, platform, chatId); + if (message === null) { + this.logger.warn({ + message: this.stringService.getWebhookUnsupportedPlatform(platform), + service: this.SERVICE_NAME, + method: 'sendWebhookNotification', + platform + }); + return false; + } + + if (platform === 'telegram') { + if (!botToken || !chatId) { + return false; + } + url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; + } + + try { + const response = await this.networkService.requestWebhook(platform, url, message); + return response.status; + } catch (error) { + this.logger.error({ + message: this.stringService.getWebhookSendError(platform), + service: this.SERVICE_NAME, + method: 'sendWebhookNotification', + error: error.message, + stack: error.stack, + url, + platform, + requestPayload: message + }); + return false; + } + } /** * Sends an email notification for hardware infrastructure alerts diff --git a/Server/service/stringService.js b/Server/service/stringService.js index 1ef208d6c..e3ab03ca5 100644 --- a/Server/service/stringService.js +++ b/Server/service/stringService.js @@ -165,6 +165,36 @@ class StringService { return this.translationService.getTranslation('maintenanceWindowEdit'); } + // Webhook Messages + get webhookUnsupportedPlatform() { + return this.translationService.getTranslation('webhookUnsupportedPlatform'); + } + + get webhookSendError() { + return this.translationService.getTranslation('webhookSendError'); + } + + get webhookSendSuccess() { + return this.translationService.getTranslation('webhookSendSuccess'); + } + + getWebhookUnsupportedPlatform(platform) { + return this.translationService.getTranslation('webhookUnsupportedPlatform') + .replace('{platform}', platform); + } + + getWebhookSendError(platform) { + return this.translationService.getTranslation('webhookSendError') + .replace('{platform}', platform); + } + + getMonitorStatus(name, status, url) { + return this.translationService.getTranslation('monitorStatus') + .replace('{name}', name) + .replace('{status}', status ? "up" : "down") + .replace('{url}', url); + } + // Error Messages get unknownError() { return this.translationService.getTranslation('unknownError'); From c4168a5d75f839186386b4fb3cb5f2f4dce08dae Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Feb 2025 09:24:21 -0800 Subject: [PATCH 27/31] Removed null and replace with undefined. --- Server/service/notificationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index be8432336..1bd4d8f8b 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -48,7 +48,7 @@ class NotificationService { if (platform === 'discord') { return { content: messageText }; } - return null; + return undefined; } /** From afc9d46bd7cbc5fb7a4e6fc97df4922a0a81f127 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Feb 2025 09:39:28 -0800 Subject: [PATCH 28/31] Refactored notification messsages into an array of acceptable types. --- Server/service/notificationService.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 1bd4d8f8b..3f4efb403 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -1,5 +1,12 @@ const SERVICE_NAME = "NotificationService"; const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot"; +const PLATFORM_TYPES = ['telegram', 'slack', 'discord']; + +const MESSAGE_FORMATTERS = { + telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }), + slack: (messageText) => ({ text: messageText }), + discord: (messageText) => ({ content: messageText }) +}; class NotificationService { static SERVICE_NAME = SERVICE_NAME; @@ -39,16 +46,11 @@ class NotificationService { monitor.url ); - if (platform === 'telegram') { - return { chat_id: chatId, text: messageText }; + if (!PLATFORM_TYPES.includes(platform)) { + return undefined; } - if (platform === 'slack') { - return { text: messageText }; - } - if (platform === 'discord') { - return { content: messageText }; - } - return undefined; + + return MESSAGE_FORMATTERS[platform](messageText, chatId); } /** From 03a938de8af77bdc5be6ac3dcc4b5f5abd43041b Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Feb 2025 09:55:19 -0800 Subject: [PATCH 29/31] Check if platform type is accepted before formatting the message. --- Server/service/notificationService.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Server/service/notificationService.js b/Server/service/notificationService.js index 3f4efb403..2546df3f9 100644 --- a/Server/service/notificationService.js +++ b/Server/service/notificationService.js @@ -72,10 +72,9 @@ class NotificationService { const { monitor, status } = networkResponse; const { platform } = notification; const { webhookUrl, botToken, chatId } = notification.config; - let url = webhookUrl; - const message = this.formatNotificationMessage(monitor, status, platform, chatId); - if (message === null) { + // Early return if platform is not supported + if (!PLATFORM_TYPES.includes(platform)) { this.logger.warn({ message: this.stringService.getWebhookUnsupportedPlatform(platform), service: this.SERVICE_NAME, @@ -85,13 +84,25 @@ class NotificationService { return false; } + // Early return for telegram if required fields are missing + if (platform === 'telegram' && (!botToken || !chatId)) { + this.logger.warn({ + message: 'Missing required fields for Telegram notification', + service: this.SERVICE_NAME, + method: 'sendWebhookNotification', + platform + }); + return false; + } + + let url = webhookUrl; if (platform === 'telegram') { - if (!botToken || !chatId) { - return false; - } url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; } + // Now that we know the platform is valid, format the message + const message = this.formatNotificationMessage(monitor, status, platform, chatId); + try { const response = await this.networkService.requestWebhook(platform, url, message); return response.status; @@ -108,7 +119,7 @@ class NotificationService { }); return false; } - } + } /** * Sends an email notification for hardware infrastructure alerts From 5be17af4c1653f2fc21075b3b60705e4006d3852 Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Feb 2025 17:29:45 -0800 Subject: [PATCH 30/31] Used 1 validation schema for all platforms. --- Server/controllers/notificationController.js | 3 - Server/validation/joi.js | 105 +++++++++---------- 2 files changed, 50 insertions(+), 58 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index 7681d7c2a..ec12db928 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -1,8 +1,5 @@ import { triggerNotificationBodyValidation, - telegramWebhookConfigValidation, - discordWebhookConfigValidation, - slackWebhookConfigValidation } from '../validation/joi.js'; import { handleError, handleValidationError } from './controllerUtils.js'; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 1b285b0a5..0967d9150 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -475,39 +475,53 @@ const imageValidation = joi "any.required": "Image file is required", }); - const telegramWebhookConfigValidation = joi.object({ - webhookUrl: joi.string().uri().optional(), - botToken: joi.string().required().messages({ - 'string.empty': 'Telegram bot token is required', - 'any.required': 'Telegram bot token is required' - }), - chatId: joi.string().required().messages({ - 'string.empty': 'Telegram chat ID is required', - 'any.required': 'Telegram chat ID is required' - }) - }); + const webhookConfigValidation = joi.object({ + webhookUrl: joi.string().uri() + .when('$platform', { + switch: [ + { + is: 'telegram', + then: joi.optional() + }, + { + is: 'discord', + then: joi.required().messages({ + 'string.empty': 'Discord webhook URL is required', + 'string.uri': 'Discord webhook URL must be a valid URL', + 'any.required': 'Discord webhook URL is required' + }) + }, + { + is: 'slack', + then: joi.required().messages({ + 'string.empty': 'Slack webhook URL is required', + 'string.uri': 'Slack webhook URL must be a valid URL', + 'any.required': 'Slack webhook URL is required' + }) + } + ] + }), + botToken: joi.string() + .when('$platform', { + is: 'telegram', + then: joi.required().messages({ + 'string.empty': 'Telegram bot token is required', + 'any.required': 'Telegram bot token is required' + }), + otherwise: joi.optional() + }), + chatId: joi.string() + .when('$platform', { + is: 'telegram', + then: joi.required().messages({ + 'string.empty': 'Telegram chat ID is required', + 'any.required': 'Telegram chat ID is required' + }), + otherwise: joi.optional() + }) + }).required(); - const discordWebhookConfigValidation = joi.object({ - webhookUrl: joi.string().uri().required().messages({ - 'string.empty': 'Discord webhook URL is required', - 'string.uri': 'Discord webhook URL must be a valid URL', - 'any.required': 'Discord webhook URL is required' - }), - botToken: joi.string().optional(), - chatId: joi.string().optional() - }); - - const slackWebhookConfigValidation = joi.object({ - webhookUrl: joi.string().uri().required().messages({ - 'string.empty': 'Slack webhook URL is required', - 'string.uri': 'Slack webhook URL must be a valid URL', - 'any.required': 'Slack webhook URL is required' - }), - botToken: joi.string().optional(), - chatId: joi.string().optional() - }); - - const triggerNotificationBodyValidation = joi.object({ + const triggerNotificationBodyValidation = joi.object({ monitorId: joi.string().required().messages({ 'string.empty': 'Monitor ID is required', 'any.required': 'Monitor ID is required' @@ -517,31 +531,14 @@ const imageValidation = joi 'any.required': 'Notification type is required', 'any.only': 'Notification type must be webhook' }), - platform: joi.string().valid('telegram', 'discord', 'slack').required().messages({ 'string.empty': 'Platform type is required', 'any.required': 'Platform type is required', 'any.only': 'Platform must be telegram, discord, or slack' }), - config: joi.alternatives() - .conditional('platform', [ - { - is: 'telegram', - then: telegramWebhookConfigValidation - }, - { - is: 'discord', - then: discordWebhookConfigValidation - }, - { - is: 'slack', - then: slackWebhookConfigValidation - } - ]) - .required() - .messages({ - 'any.required': 'Webhook configuration is required' - }) + config: webhookConfigValidation.required().messages({ + 'any.required': 'Webhook configuration is required' + }) }); export { @@ -604,7 +601,5 @@ export { getStatusPageQueryValidation, imageValidation, triggerNotificationBodyValidation, - telegramWebhookConfigValidation, - discordWebhookConfigValidation, - slackWebhookConfigValidation + webhookConfigValidation, }; From 4ef2b294bb6239527cb9ed375380b5561eb8beaf Mon Sep 17 00:00:00 2001 From: Skorpios Date: Mon, 17 Feb 2025 17:33:54 -0800 Subject: [PATCH 31/31] Used string service instead of hardcoded value. --- Server/controllers/notificationController.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Server/controllers/notificationController.js b/Server/controllers/notificationController.js index ec12db928..bce20ea13 100644 --- a/Server/controllers/notificationController.js +++ b/Server/controllers/notificationController.js @@ -22,17 +22,17 @@ class NotificationController { next(handleValidationError(error, SERVICE_NAME)); return; } - + try { const { monitorId, type, platform, config } = req.body; - + const networkResponse = { monitor: { _id: monitorId, name: "Test Monitor", url: "http://www.google.com" }, status: false, statusChanged: true, prevStatus: true, }; - + if (type === "webhook") { const notification = { type, @@ -45,11 +45,11 @@ class NotificationController { notification ); } - + return res.success({ - msg: "Notification sent successfully" + msg: this.stringService.webhookSendSuccess }); - + } catch (error) { next(handleError(error, SERVICE_NAME, "triggerNotification")); }