diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
index 303453655..69e03fd5e 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/useNotifications.js
@@ -98,12 +98,9 @@ const useGetNotificationById = (id, setNotification) => {
const notification = response?.data?.data ?? null;
const notificationData = {
- userId: notification?.userId,
- teamId: notification?.teamId,
address: notification?.address,
notificationName: notification?.notificationName,
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
- config: notification?.config,
};
setNotification(notificationData);
diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx
index 2fd83e4e1..fba30185f 100644
--- a/client/src/Pages/Notifications/create/index.jsx
+++ b/client/src/Pages/Notifications/create/index.jsx
@@ -17,15 +17,17 @@ import {
useEditNotification,
useTestNotification,
} from "../../../Hooks/useNotifications";
-import {
- notificationEmailValidation,
- notificationWebhookValidation,
- notificationPagerDutyValidation,
-} from "../../../Validation/validation";
+import { notificationValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
-import { NOTIFICATION_TYPES } from "../utils";
+import {
+ NOTIFICATION_TYPES,
+ TITLE_MAP,
+ DESCRIPTION_MAP,
+ LABEL_MAP,
+ PLACEHOLDER_MAP,
+} from "../utils";
// Setup
@@ -49,11 +51,6 @@ const CreateNotifications = () => {
notificationName: "",
address: "",
type: NOTIFICATION_TYPES[0]._id,
- config: {
- webhookUrl: "",
- platform: "",
- routingKey: "",
- },
});
const [errors, setErrors] = useState({});
const { t } = useTranslation();
@@ -71,42 +68,17 @@ const CreateNotifications = () => {
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
- if (form.type === "slack" || form.type === "discord") {
- form.type = "webhook";
- }
-
let error = null;
- if (form.type === "email") {
- error = notificationEmailValidation.validate(
- { notificationName: form.notificationName, address: form.address },
- { abortEarly: false }
- ).error;
- } else if (form.type === "webhook") {
- form.config = {
- platform: form.config.platform,
- webhookUrl: form.config.webhookUrl,
- };
- error = notificationWebhookValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- } else if (form.type === "pager_duty") {
- form.config = {
- platform: form.config.platform,
- routingKey: form.config.routingKey,
- };
- error = notificationPagerDutyValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- }
+ error = notificationValidation.validate(form, { abortEarly: false }).error;
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
+ console.log(JSON.stringify(newErrors));
+ console.log(JSON.stringify(form, null, 2));
createToast({ body: "Please check the form for errors." });
setErrors(newErrors);
return;
@@ -124,63 +96,10 @@ const CreateNotifications = () => {
const newNotification = { ...notification, [name]: value };
- // Handle config/platform initialization if type is webhook
-
- const type = NOTIFICATION_TYPES.find(
- (type) => type._id === newNotification.type
- ).value;
-
- if (type === "email") {
- newNotification.config = null;
- } else if (type === "slack" || type === "webhook" || type === "discord") {
- newNotification.address = "";
- newNotification.config = newNotification.config || {};
- if (name === "config") {
- newNotification.config = value;
- }
- if (type === "webhook") {
- newNotification.config.platform = "webhook";
- }
- if (type === "slack") {
- newNotification.config.platform = "slack";
- }
- if (type === "discord") {
- newNotification.config.platform = "discord";
- }
- } else if (type === "pager_duty") {
- newNotification.config = newNotification.config || {};
- if (name === "config") {
- newNotification.config = value;
- }
- newNotification.config.platform = "pager_duty";
- }
-
- // Field-level validation
- let fieldError;
-
- if (name === "notificationName") {
- const { error } = notificationEmailValidation.extract(name).validate(value);
- fieldError = error?.message;
- }
-
- if (type === "email" && name === "address") {
- const { error } = notificationEmailValidation.extract(name).validate(value);
- fieldError = error?.message;
- }
-
- if (
- (type === "slack" || type === "webhook" || type === "discord") &&
- name === "config"
- ) {
- // Validate only webhookUrl inside config
- const { error } = notificationWebhookValidation.extract("config").validate(value);
- fieldError = error?.message;
- }
-
- // Set field-level error
+ const { error } = notificationValidation.extract(name).validate(value);
setErrors((prev) => ({
...prev,
- [name]: fieldError,
+ [name]: error?.message,
}));
setNotification(newNotification);
@@ -192,36 +111,9 @@ const CreateNotifications = () => {
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
- if (form.type === "slack" || form.type === "discord") {
- form.type = "webhook";
- }
-
let error = null;
- if (form.type === "email") {
- error = notificationEmailValidation.validate(
- { notificationName: form.notificationName, address: form.address },
- { abortEarly: false }
- ).error;
- } else if (form.type === "webhook") {
- form.config = {
- platform: form.config.platform,
- webhookUrl: form.config.webhookUrl,
- };
- error = notificationWebhookValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- } else if (form.type === "pager_duty") {
- form.config = {
- platform: form.config.platform,
- routingKey: form.config.routingKey,
- };
- error = notificationPagerDutyValidation.validate(
- { notificationName: form.notificationName, config: form.config },
- { abortEarly: false }
- ).error;
- }
+ error = notificationValidation.validate(form, { abortEarly: false }).error;
if (error) {
const newErrors = {};
@@ -236,6 +128,7 @@ const CreateNotifications = () => {
testNotification(form);
};
+ const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
return (
@@ -287,164 +180,24 @@ const CreateNotifications = () => {
/>
- {notification.type === 1 && (
-
-
-
- {t("createNotifications.emailSettings.title")}
-
-
- {t("createNotifications.emailSettings.description")}
-
-
-
-
-
-
- )}
- {notification.type === 2 && (
-
-
-
- {t("createNotifications.slackSettings.title")}
-
-
- {t("createNotifications.slackSettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- webhookUrl: e.target.value,
- };
+
+
+ {t(TITLE_MAP[type])}
+ {t(DESCRIPTION_MAP[type])}
+
+
+
+
+
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {notification.type === 3 && (
-
-
-
- {t("createNotifications.pagerdutySettings.title")}
-
-
- {t("createNotifications.pagerdutySettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- routingKey: e.target.value,
- };
-
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {notification.type === 4 && (
-
-
-
- {t("createNotifications.genericWebhookSettings.title")}
-
-
- {t("createNotifications.genericWebhookSettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- webhookUrl: e.target.value,
- };
-
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
- {notification.type === 5 && (
-
-
-
- {t("createNotifications.discordSettings.title")}
-
-
- {t("createNotifications.discordSettings.description")}
-
-
-
- {
- const updatedConfig = {
- ...notification.config,
- webhookUrl: e.target.value,
- };
-
- onChange({
- target: {
- name: "config",
- value: updatedConfig,
- },
- });
- }}
- />
-
-
- )}
{
return row.notificationName;
},
},
- {
- id: "type",
- content: "Type",
- render: (row) => {
- return row.type;
- },
- },
{
id: "target",
content: "Target",
render: (row) => {
- const type = row.type;
- if (type === "email") {
- return row.address;
- }
- if (type === "webhook") {
- return row.config?.webhookUrl;
- }
- if (type === "pager_duty") {
- return row.config?.routingKey;
- }
+ return row.address;
},
},
{
diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js
index 545791246..7ef6e5a60 100644
--- a/client/src/Pages/Notifications/utils.js
+++ b/client/src/Pages/Notifications/utils.js
@@ -5,3 +5,35 @@ export const NOTIFICATION_TYPES = [
{ _id: 4, name: "Webhook", value: "webhook" },
{ _id: 5, name: "Discord", value: "discord" },
];
+
+export const TITLE_MAP = {
+ email: "createNotifications.emailSettings.title",
+ slack: "createNotifications.slackSettings.title",
+ pager_duty: "createNotifications.pagerDutySettings.title",
+ webhook: "createNotifications.webhookSettings.title",
+ discord: "createNotifications.discordSettings.title",
+};
+
+export const DESCRIPTION_MAP = {
+ email: "createNotifications.emailSettings.description",
+ slack: "createNotifications.slackSettings.description",
+ pager_duty: "createNotifications.pagerDutySettings.description",
+ webhook: "createNotifications.webhookSettings.description",
+ discord: "createNotifications.discordSettings.description",
+};
+
+export const LABEL_MAP = {
+ email: "createNotifications.emailSettings.emailLabel",
+ slack: "createNotifications.slackSettings.webhookLabel",
+ pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel",
+ webhook: "createNotifications.webhookSettings.webhookLabel",
+ discord: "createNotifications.discordSettings.webhookLabel",
+};
+
+export const PLACEHOLDER_MAP = {
+ email: "createNotifications.emailSettings.emailPlaceholder",
+ slack: "createNotifications.slackSettings.webhookPlaceholder",
+ pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder",
+ webhook: "createNotifications.webhookSettings.webhookPlaceholder",
+ discord: "createNotifications.discordSettings.webhookPlaceholder",
+};
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 02d957063..4e6a806c8 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -404,57 +404,19 @@ const infrastructureMonitorValidation = joi.object({
notifications: joi.array().items(joi.string()),
});
-const notificationEmailValidation = joi.object({
+const notificationValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
- address: joi
- .string()
- .email({ tlds: { allow: false } })
- .required()
- .messages({
- "string.empty": "E-mail address cannot be empty",
-
- "string.email": "Please enter a valid e-mail address",
- "string.base": "E-mail address must be a string",
- "any.required": "E-mail address is required",
- }),
-});
-const notificationWebhookValidation = joi.object({
- notificationName: joi.string().required().messages({
- "string.empty": "Notification name is required",
- "any.required": "Notification name is required",
+ address: joi.string().required().messages({
+ "string.empty": "This field cannot be empty",
+ "string.base": "This field must be a string",
+ "any.required": "This field is required",
}),
- config: joi
- .object({
- webhookUrl: joi.string().uri().required().messages({
- "string.empty": "Webhook URL is required",
- "string.uri": "Webhook URL must be a valid URI",
- "any.required": "Webhook URL is required",
- }),
- platform: joi.string().required().messages({
- "string.base": "Platform must be a string",
- "any.required": "Platform is required",
- }),
- })
- .unknown(true),
-});
-
-const notificationPagerDutyValidation = joi.object({
- notificationName: joi.string().required().messages({
- "string.empty": "Notification name is required",
- "any.required": "Notification name is required",
- }),
- config: joi.object({
- platform: joi.string().required().messages({
- "string.base": "Platform must be a string",
- "any.required": "Platform is required",
- }),
- routingKey: joi.string().required().messages({
- "string.empty": "Routing key is required",
- "any.required": "Routing key is required",
- }),
+ type: joi.string().required().messages({
+ "string.empty": "This field is required",
+ "any.required": "This field is required",
}),
});
@@ -469,7 +431,5 @@ export {
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation,
- notificationEmailValidation,
- notificationWebhookValidation,
- notificationPagerDutyValidation,
+ notificationValidation,
};
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index fd89d31f5..a7d9fe069 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -707,13 +707,13 @@
"webhookLabel": "Slack webhook URL",
"webhookPlaceholder": "https://hooks.slack.com/services/..."
},
- "pagerdutySettings": {
+ "pagerDutySettings": {
"title": "PagerDuty",
"description": "Configure your PagerDuty integration here",
"integrationKeyLabel": "Integration key",
"integrationKeyPlaceholder": "1234567890"
},
- "genericWebhookSettings": {
+ "webhookSettings": {
"title": "Webhook",
"description": "Configure your webhook here",
"webhookLabel": "Webhook URL",
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
index 9fee7803e..e7e2a1f2a 100755
--- a/server/controllers/notificationController.js
+++ b/server/controllers/notificationController.js
@@ -23,85 +23,13 @@ class NotificationController {
this.stringService = stringService;
this.statusService = statusService;
this.db = db;
- this.triggerNotification = this.triggerNotification.bind(this);
- }
-
- 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, status = false } = req.body;
-
- // Create a simplified networkResponse similar to what would come from monitoring
- const networkResponse = {
- monitorId,
- status,
- };
-
- // Use the statusService to get monitor details and handle status change logic
- // This returns { monitor, statusChanged, prevStatus } exactly like your job queue uses
- const statusResult = await this.statusService.updateStatus(networkResponse);
-
- if (type === NOTIFICATION_TYPES.WEBHOOK) {
- const notification = {
- type,
- platform,
- config,
- };
-
- await this.notificationService.sendWebhookNotification(
- statusResult, // Contains monitor, statusChanged, and prevStatus
- notification
- );
- }
-
- return res.success({
- msg: this.stringService.webhookSendSuccess,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "triggerNotification"));
- }
- }
-
- createTestNetworkResponse() {
- return {
- monitor: {
- _id: "test-monitor-id",
- name: "Test Monitor",
- url: "https://example.com",
- },
- status: true,
- statusChanged: true,
- prevStatus: false,
- };
}
testNotification = async (req, res, next) => {
try {
const notification = req.body;
- let success;
- if (notification?.type === "email") {
- success = await this.notificationService.sendTestEmail(notification);
- }
-
- if (notification?.type === "webhook") {
- success =
- await this.notificationService.sendTestWebhookNotification(notification);
- }
-
- if (notification?.type === "pager_duty") {
- success =
- await this.notificationService.sendTestPagerDutyNotification(notification);
- }
+ const success = await this.notificationService.sendTestNotification(notification);
if (!success) {
return res.error({
diff --git a/server/db/models/Monitor.js b/server/db/models/Monitor.js
index 7bb38f520..38aeff0c3 100755
--- a/server/db/models/Monitor.js
+++ b/server/db/models/Monitor.js
@@ -69,6 +69,15 @@ const MonitorSchema = mongoose.Schema(
type: Number,
default: undefined,
},
+ notifications: [
+ {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Notification",
+ },
+ ],
+ secret: {
+ type: String,
+ },
thresholds: {
type: {
usage_cpu: { type: Number },
@@ -78,14 +87,33 @@ const MonitorSchema = mongoose.Schema(
},
_id: false,
},
- notifications: [
- {
- type: mongoose.Schema.Types.ObjectId,
- ref: "Notification",
+ alertThreshold: {
+ type: Number,
+ default: 5,
+ },
+ cpuAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
+ memoryAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
+ diskAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
+ tempAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
},
- ],
- secret: {
- type: String,
},
},
{
@@ -132,6 +160,33 @@ MonitorSchema.pre("deleteMany", async function (next) {
next();
});
+MonitorSchema.pre("save", function (next) {
+ if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) {
+ this.cpuAlertThreshold = this.alertThreshold;
+ }
+ if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) {
+ this.memoryAlertThreshold = this.alertThreshold;
+ }
+ if (!this.diskAlertThreshold || this.isModified("alertThreshold")) {
+ this.diskAlertThreshold = this.alertThreshold;
+ }
+ if (!this.tempAlertThreshold || this.isModified("alertThreshold")) {
+ this.tempAlertThreshold = this.alertThreshold;
+ }
+ next();
+});
+
+MonitorSchema.pre("findOneAndUpdate", function (next) {
+ const update = this.getUpdate();
+ if (update.alertThreshold) {
+ update.cpuAlertThreshold = update.alertThreshold;
+ update.memoryAlertThreshold = update.alertThreshold;
+ update.diskAlertThreshold = update.alertThreshold;
+ update.tempAlertThreshold = update.alertThreshold;
+ }
+ next();
+});
+
MonitorSchema.index({ teamId: 1, type: 1 });
export default mongoose.model("Monitor", MonitorSchema);
diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js
index 751324131..a6a3c7c09 100755
--- a/server/db/models/Notification.js
+++ b/server/db/models/Notification.js
@@ -1,19 +1,5 @@
import mongoose from "mongoose";
-const configSchema = mongoose.Schema(
- {
- webhookUrl: { type: String },
- botToken: { type: String },
- chatId: { type: String },
- platform: {
- type: String,
- enum: ["slack", "pager_duty", "webhook", "discord"],
- },
- routingKey: { type: String },
- },
- { _id: false }
-);
-
const NotificationSchema = mongoose.Schema(
{
userId: {
@@ -30,14 +16,7 @@ const NotificationSchema = mongoose.Schema(
},
type: {
type: String,
- enum: ["email", "sms", "webhook", "pager_duty"],
- },
- platform: {
- type: String,
- },
- config: {
- type: configSchema,
- default: () => ({}),
+ enum: ["email", "slack", "discord", "webhook", "pager_duty"],
},
notificationName: {
type: String,
@@ -49,34 +28,6 @@ const NotificationSchema = mongoose.Schema(
phone: {
type: String,
},
- alertThreshold: {
- type: Number,
- default: 5,
- },
- cpuAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
- memoryAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
- diskAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
- tempAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
},
{
timestamps: true,
diff --git a/server/index.js b/server/index.js
index 0753af995..99be6de7a 100755
--- a/server/index.js
+++ b/server/index.js
@@ -73,6 +73,7 @@ import StatusService from "./service/statusService.js";
// Notification Service and dependencies
import NotificationService from "./service/notificationService.js";
+import NotificationUtils from "./service/notificationUtils.js";
// Buffer Service and dependencies
import BufferService from "./service/bufferService.js";
@@ -173,13 +174,19 @@ const startApp = async () => {
);
const bufferService = new BufferService({ db, logger });
const statusService = new StatusService({ db, logger, buffer: bufferService });
- const notificationService = new NotificationService(
+ const notificationUtils = new NotificationUtils({
+ stringService,
+ emailService,
+ });
+
+ const notificationService = new NotificationService({
emailService,
db,
logger,
networkService,
- stringService
- );
+ stringService,
+ notificationUtils,
+ });
const redisService = new RedisService({ Redis: IORedis, logger });
diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js
index a937975c5..164c0afcc 100755
--- a/server/routes/notificationRoute.js
+++ b/server/routes/notificationRoute.js
@@ -11,8 +11,6 @@ class NotificationRoutes {
initializeRoutes() {
this.router.use(verifyJWT);
- this.router.post("/trigger", this.notificationController.triggerNotification);
-
this.router.post("/test", this.notificationController.testNotification);
this.router.post("/test/all", this.notificationController.testAllNotifications);
diff --git a/server/service/PulseQueue/PulseQueue.js b/server/service/PulseQueue/PulseQueue.js
index fd420400f..0452985a0 100644
--- a/server/service/PulseQueue/PulseQueue.js
+++ b/server/service/PulseQueue/PulseQueue.js
@@ -1,5 +1,4 @@
import { Pulse } from "@pulsecron/pulse";
-import { ObjectId } from "mongodb";
const SERVICE_NAME = "PulseQueue";
class PulseQueue {
diff --git a/server/service/networkService.js b/server/service/networkService.js
index 5ad28e594..08b2f065e 100755
--- a/server/service/networkService.js
+++ b/server/service/networkService.js
@@ -432,9 +432,9 @@ class NetworkService {
throw err;
}
- async requestWebhook(platform, url, message) {
+ async requestWebhook(type, url, body) {
try {
- const response = await this.axios.post(url, message, {
+ const response = await this.axios.post(url, body, {
headers: {
"Content-Type": "application/json",
},
@@ -444,7 +444,7 @@ class NetworkService {
type: "webhook",
status: true,
code: response.status,
- message: `Successfully sent ${platform} notification`,
+ message: `Successfully sent ${type} notification`,
payload: response.data,
};
} catch (error) {
@@ -452,19 +452,13 @@ class NetworkService {
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`,
+ message: `Failed to send ${type} notification`,
payload: error.response?.data,
};
}
diff --git a/server/service/notificationService.js b/server/service/notificationService.js
old mode 100755
new mode 100644
index dcd629d53..a2453faa9
--- a/server/service/notificationService.js
+++ b/server/service/notificationService.js
@@ -1,512 +1,123 @@
-const SERVICE_NAME = "NotificationService";
-const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot";
-const PLATFORM_TYPES = ["telegram", "slack", "discord", "webhook"];
-
-const MESSAGE_FORMATTERS = {
- telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }),
- slack: (messageText) => ({ text: messageText }),
- discord: (messageText) => ({ content: messageText }),
- webhook: (messageText) => ({ text: messageText }),
-};
-
class NotificationService {
- static SERVICE_NAME = SERVICE_NAME;
- /**
- * Creates an instance of 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, stringService) {
- this.SERVICE_NAME = SERVICE_NAME;
+ constructor({
+ emailService,
+ db,
+ logger,
+ networkService,
+ stringService,
+ notificationUtils,
+ }) {
this.emailService = emailService;
this.db = db;
this.logger = logger;
this.networkService = networkService;
this.stringService = stringService;
+ this.notificationUtils = notificationUtils;
}
- /**
- * 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.
- */
+ sendNotification = async ({ notification, subject, content, html }) => {
+ const { type, address } = notification;
- formatNotificationMessage(monitor, status, platform, chatId, code, timestamp) {
- // Format timestamp using the local system timezone
- const formatTime = (timestamp) => {
- const date = new Date(timestamp);
-
- // Get timezone abbreviation and format the date
- const timeZoneAbbr = date
- .toLocaleTimeString("en-US", { timeZoneName: "short" })
- .split(" ")
- .pop();
-
- // Format the date with readable format
- return (
- date
- .toLocaleString("en-US", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hour12: false,
- })
- .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
- " " +
- timeZoneAbbr
- );
- };
-
- // Get formatted time
- const formattedTime = timestamp
- ? formatTime(timestamp)
- : formatTime(new Date().getTime());
-
- // Create different messages based on status with extra spacing
- let messageText;
- if (status === true) {
- messageText = this.stringService.monitorUpAlert
- .replace("{monitorName}", monitor.name)
- .replace("{time}", formattedTime)
- .replace("{code}", code || "Unknown");
- } else {
- messageText = this.stringService.monitorDownAlert
- .replace("{monitorName}", monitor.name)
- .replace("{time}", formattedTime)
- .replace("{code}", code || "Unknown");
- }
-
- if (!PLATFORM_TYPES.includes(platform)) {
- return undefined;
- }
-
- return MESSAGE_FORMATTERS[platform](messageText, chatId);
- }
-
- formatHardwareNotificationMessage = (
- monitor,
- status,
- platform,
- chatId,
- code,
- timestamp,
- alerts
- ) => {
- const messageText = alerts.map((alert) => alert).join("\n");
- return MESSAGE_FORMATTERS[platform](messageText, chatId);
- };
-
- sendTestWebhookNotification = async (notification) => {
- const config = notification.config;
- const response = await this.networkService.requestWebhook(
- config.platform,
- config.webhookUrl,
- "This is a test notification"
- );
-
- return response.status;
- };
-
- /**
- * 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, alerts = []) {
- const { monitor, status, code } = networkResponse;
- const { webhookUrl, platform, botToken, chatId } = notification.config;
-
- // Early return if platform is not supported
- if (!PLATFORM_TYPES.includes(platform)) {
- this.logger.warn({
- message: this.stringService.getWebhookUnsupportedPlatform(platform),
- service: this.SERVICE_NAME,
- method: "sendWebhookNotification",
- details: { platform },
- });
- 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",
- details: { platform },
- });
- return false;
- }
-
- let url = webhookUrl;
- if (platform === "telegram") {
- url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
- }
-
- let message;
- if (monitor.type === "hardware") {
- message = this.formatHardwareNotificationMessage(
- monitor,
- status,
- platform,
- chatId,
- code,
- networkResponse.timestamp,
- alerts
- );
- } else {
- message = this.formatNotificationMessage(
- monitor,
- status,
- platform,
- chatId,
- code, // Pass the code field directly
- networkResponse.timestamp
- );
- }
-
- 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",
- stack: error.stack,
- });
- return false;
- }
- }
-
- /**
- * Sends an email notification for hardware infrastructure alerts
- *
- * @async
- * @function sendHardwareEmail
- * @param {Object} networkResponse - Response object containing monitor information
- * @param {string} address - Email address to send the notification to
- * @param {Array} [alerts=[]] - List of hardware alerts to include in the email
- * @returns {Promise} - Indicates whether email was sent successfully
- * @throws {Error}
- */
- async sendHardwareEmail(networkResponse, address, alerts = []) {
- if (alerts.length === 0) return false;
- const { monitor, status, prevStatus } = networkResponse;
- const template = "hardwareIncidentTemplate";
- const context = { monitor: monitor.name, url: monitor.url, alerts };
- const subject = `Monitor ${monitor.name} infrastructure alerts`;
- const html = await this.emailService.buildEmail(template, context);
- this.emailService.sendEmail(address, subject, html).catch((error) => {
- this.logger.warn({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "sendHardwareEmail",
- stack: error.stack,
- });
- });
-
- return true;
- }
-
- sendTestEmail = async (notification) => {
- const to = notification?.address;
- if (!to || typeof to !== "string") {
- throw new Error(this.stringService.errorForValidEmailAddress);
- }
-
- const subject = this.stringService.testEmailSubject;
- const context = { testName: "Monitoring System" };
-
- try {
- const html = await this.emailService.buildEmail("testEmailTemplate", context);
- const messageId = await this.emailService.sendEmail(to, subject, html);
-
- if (messageId) {
- return true;
- }
- } catch (error) {
- this.logger.warn({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "sendTestEmail",
- stack: error.stack,
- });
- return false;
- }
- };
-
- /**
- * Sends an email notification about monitor status change
- *
- * @async
- * @function sendEmail
- * @param {Object} networkResponse - Response object containing monitor status information
- * @param {string} address - Email address to send the notification to
- * @returns {Promise} - Indicates email was sent successfully
- */
-
- async sendEmail(networkResponse, address) {
- const { monitor, status, prevStatus } = networkResponse;
- const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
- const context = { monitor: monitor.name, url: monitor.url };
- const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
-
- const html = await this.emailService.buildEmail(template, context);
- this.emailService.sendEmail(address, subject, html).catch((error) => {
- this.logger.warn({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "sendEmail",
- stack: error.stack,
- });
- });
-
- return true;
- }
-
- async sendTestPagerDutyNotification(notification) {
- const { routingKey } = notification.config;
- const response = await this.networkService.requestPagerDuty({
- message: "This is a test notification",
- monitorUrl: "Test notification",
- routingKey,
- });
-
- return response;
- }
- async sendPagerDutyNotification(networkResponse, notification, alerts = []) {
- const { monitor, status, code } = networkResponse;
- const { routingKey, platform } = notification.config;
-
- let message;
- if (monitor.type === "hardware") {
- message = this.formatHardwareNotificationMessage(
- monitor,
- status,
- platform,
- null,
- code,
- networkResponse.timestamp,
- alerts
- );
- } else {
- 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;
- } 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
- if (networkResponse.statusChanged === false) return false;
- // if prevStatus is undefined, monitor is resuming, we're done
- if (networkResponse.prevStatus === undefined) return false;
-
- 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);
- }
- }
+ if (type === "email") {
+ const messageId = await this.emailService.sendEmail(address, subject, html);
+ if (!messageId) return false;
return true;
- } catch (error) {
- this.logger.error({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "handleNotifications",
- stack: error.stack,
- });
}
- }
- /**
- * Handles status change notifications for a monitor
- *
- * @async
- * @function handleStatusNotifications
- * @param {Object} networkResponse - Response object containing monitor status information
- * @returns {Promise} - Indicates whether notifications were processed
- * @throws {Error}
- */
- async handleHardwareNotifications(networkResponse) {
- const thresholds = networkResponse?.monitor?.thresholds;
- if (thresholds === undefined) return false; // No thresholds set, we're done
- // Get thresholds from monitor
- const {
- usage_cpu: cpuThreshold = -1,
- usage_memory: memoryThreshold = -1,
- usage_disk: diskThreshold = -1,
- } = thresholds;
+ // Create a body for webhooks
+ let body = { text: content };
+ if (type === "discord") {
+ body = { content };
+ }
- // Get metrics from response
- const metrics = networkResponse?.payload?.data ?? null;
- if (metrics === null) return false;
+ if (type === "slack" || type === "discord" || type === "webhook") {
+ const response = await this.networkService.requestWebhook(type, address, body);
+ return response.status;
+ }
+ if (type === "pager_duty") {
+ const response = await this.networkService.requestPagerDuty({
+ message: content,
+ monitorUrl: subject,
+ routingKey: address,
+ });
- const {
- cpu: { usage_percent: cpuUsage = -1 } = {},
- memory: { usage_percent: memoryUsage = -1 } = {},
- disk = [],
- } = metrics;
+ return response;
+ }
+ };
- const alerts = {
- cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
- memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
- disk:
- disk?.some(
- (d) =>
- diskThreshold !== -1 &&
- typeof d?.usage_percent === "number" &&
- d?.usage_percent > diskThreshold
- ) ?? false,
- };
+ async handleNotifications(networkResponse) {
+ const { monitor, statusChanged, prevStatus } = networkResponse;
+ const { type } = monitor;
+ if (type !== "hardware" && statusChanged === false) return false;
+ // if prevStatus is undefined, monitor is resuming, we're done
+ if (type !== "hardware" && prevStatus === undefined) return false;
const notificationIDs = networkResponse.monitor?.notifications ?? [];
- const notifications = await this.db.getNotificationsByIds(notificationIDs);
+ if (notificationIDs.length === 0) return false;
- for (const notification of notifications) {
- const alertsToSend = [];
- const alertTypes = ["cpu", "memory", "disk"];
+ if (networkResponse.monitor.type === "hardware") {
+ const thresholds = networkResponse?.monitor?.thresholds;
- for (const type of alertTypes) {
- // Iterate over each alert type to see if any need to be decremented
- if (alerts[type] === true) {
- notification[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
+ if (thresholds === undefined) return false; // No thresholds set, we're done
+ const metrics = networkResponse?.payload?.data ?? null;
+ if (metrics === null) return false; // No metrics, we're done
- if (notification[`${type}AlertThreshold`] <= 0) {
- // If threshold drops below 0, reset and send notification
- notification[`${type}AlertThreshold`] = notification.alertThreshold;
+ const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse);
+ if (alerts.length === 0) return false;
- const formatAlert = {
- cpu: () =>
- `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
- memory: () =>
- `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
- disk: () =>
- `Your current disk usage: ${disk
- .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
- .join(
- ", "
- )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
- };
- alertsToSend.push(formatAlert[type]());
- }
- }
- }
+ const { subject, html } = await this.notificationUtils.buildHardwareEmail(
+ networkResponse,
+ alerts
+ );
+ const content =
+ await this.notificationUtils.buildHardwareNotificationMessage(alerts);
- await notification.save();
-
- if (alertsToSend.length === 0) continue; // No alerts to send, we're done
-
- if (notification.type === "email") {
- await this.sendHardwareEmail(networkResponse, notification.address, alertsToSend);
- } else if (notification.type === "webhook") {
- await this.sendWebhookNotification(networkResponse, notification, alertsToSend);
- } else if (notification.type === "pager_duty") {
- await this.sendPagerDutyNotification(networkResponse, notification, alertsToSend);
- }
+ const success = await this.notifyAll({ notificationIDs, subject, html, content });
+ return success;
}
- return true;
+
+ // Status monitors
+ const { subject, html } =
+ await this.notificationUtils.buildStatusEmail(networkResponse);
+ const content = await this.notificationUtils.buildWebhookMessage(networkResponse);
+ const success = this.notifyAll({ notificationIDs, subject, html, content });
+ return success;
}
- /**
- * Handles notifications for different monitor types
- *
- * @async
- * @function handleNotifications
- * @param {Object} networkResponse - Response object containing monitor information
- * @returns {Promise} - Indicates whether notifications were processed successfully
- */
- async handleNotifications(networkResponse) {
- try {
- if (networkResponse.monitor.type === "hardware") {
- this.handleHardwareNotifications(networkResponse);
- }
- this.handleStatusNotifications(networkResponse);
- return true;
- } catch (error) {
- this.logger.error({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "handleNotifications",
- stack: error.stack,
- });
- }
- }
-
- async testAllNotifications(notificationIDs) {
+ async notifyAll({ notificationIDs, subject, html, content }) {
const notifications = await this.db.getNotificationsByIds(notificationIDs);
// Map each notification to a test promise
- const testPromises = notifications.map(async (notification) => {
+ const promises = notifications.map(async (notification) => {
try {
- if (notification.type === "email") {
- await this.sendTestEmail(notification);
- } else if (notification.type === "webhook") {
- await this.sendTestWebhookNotification(notification);
- } else if (notification.type === "pager_duty") {
- await this.sendTestPagerDutyNotification(notification);
- }
+ await this.sendNotification({ notification, subject, content, html });
return true;
} catch (err) {
return false;
}
});
- const results = await Promise.all(testPromises);
+ const results = await Promise.all(promises);
return results.every((r) => r === true);
}
+
+ async getTestNotification() {
+ const html = await this.notificationUtils.buildTestEmail();
+ const content = "This is a test notification";
+ const subject = "Test Notification";
+ return { subject, html, content };
+ }
+
+ async testAllNotifications(notificationIDs) {
+ const { subject, html, content } = await this.getTestNotification();
+ return this.notifyAll({ notificationIDs, subject, html, content });
+ }
+
+ async sendTestNotification(notification) {
+ const { subject, html, content } = await this.getTestNotification();
+ const success = await this.sendNotification({ notification, subject, content, html });
+ return success;
+ }
}
export default NotificationService;
diff --git a/server/service/notificationUtils.js b/server/service/notificationUtils.js
new file mode 100644
index 000000000..61c3ec36a
--- /dev/null
+++ b/server/service/notificationUtils.js
@@ -0,0 +1,146 @@
+class NotificationUtils {
+ constructor({ stringService, emailService }) {
+ this.stringService = stringService;
+ this.emailService = emailService;
+ }
+
+ buildTestEmail = async () => {
+ const context = { testName: "Monitoring System" };
+ const html = await this.emailService.buildEmail("testEmailTemplate", context);
+ return html;
+ };
+
+ buildStatusEmail = async (networkResponse) => {
+ const { monitor, status, prevStatus } = networkResponse;
+ const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
+ const context = { monitor: monitor.name, url: monitor.url };
+ const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
+ const html = await this.emailService.buildEmail(template, context);
+ return { subject, html };
+ };
+
+ buildWebhookMessage = (networkResponse) => {
+ const { monitor, status, code, timestamp } = networkResponse;
+ // Format timestamp using the local system timezone
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+
+ // Get timezone abbreviation and format the date
+ const timeZoneAbbr = date
+ .toLocaleTimeString("en-US", { timeZoneName: "short" })
+ .split(" ")
+ .pop();
+
+ // Format the date with readable format
+ return (
+ date
+ .toLocaleString("en-US", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ })
+ .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
+ " " +
+ timeZoneAbbr
+ );
+ };
+
+ // Get formatted time
+ const formattedTime = timestamp
+ ? formatTime(timestamp)
+ : formatTime(new Date().getTime());
+
+ // Create different messages based on status with extra spacing
+ let messageText;
+ if (status === true) {
+ messageText = this.stringService.monitorUpAlert
+ .replace("{monitorName}", monitor.name)
+ .replace("{time}", formattedTime)
+ .replace("{code}", code || "Unknown");
+ } else {
+ messageText = this.stringService.monitorDownAlert
+ .replace("{monitorName}", monitor.name)
+ .replace("{time}", formattedTime)
+ .replace("{code}", code || "Unknown");
+ }
+ return messageText;
+ };
+
+ buildHardwareAlerts = async (networkResponse) => {
+ const monitor = networkResponse?.monitor;
+ const thresholds = networkResponse?.monitor?.thresholds;
+ const {
+ usage_cpu: cpuThreshold = -1,
+ usage_memory: memoryThreshold = -1,
+ usage_disk: diskThreshold = -1,
+ } = thresholds;
+
+ const metrics = networkResponse?.payload?.data;
+ const {
+ cpu: { usage_percent: cpuUsage = -1 } = {},
+ memory: { usage_percent: memoryUsage = -1 } = {},
+ disk = [],
+ } = metrics;
+
+ const alerts = {
+ cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
+ memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
+ disk:
+ disk?.some(
+ (d) =>
+ diskThreshold !== -1 &&
+ typeof d?.usage_percent === "number" &&
+ d?.usage_percent > diskThreshold
+ ) ?? false,
+ };
+
+ const alertsToSend = [];
+ const alertTypes = ["cpu", "memory", "disk"];
+ for (const type of alertTypes) {
+ // Iterate over each alert type to see if any need to be decremented
+ if (alerts[type] === true) {
+ monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
+
+ if (monitor[`${type}AlertThreshold`] <= 0) {
+ // If threshold drops below 0, reset and send notification
+ monitor[`${type}AlertThreshold`] = monitor.alertThreshold;
+
+ const formatAlert = {
+ cpu: () =>
+ `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
+ memory: () =>
+ `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
+ disk: () =>
+ `Your current disk usage: ${disk
+ .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
+ .join(
+ ", "
+ )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
+ };
+ alertsToSend.push(formatAlert[type]());
+ }
+ }
+ }
+ await monitor.save();
+ return alertsToSend;
+ };
+
+ buildHardwareEmail = async (networkResponse, alerts) => {
+ const { monitor } = networkResponse;
+ const template = "hardwareIncidentTemplate";
+ const context = { monitor: monitor.name, url: monitor.url, alerts };
+ const subject = `Monitor ${monitor.name} infrastructure alerts`;
+ const html = await this.emailService.buildEmail(template, context);
+ return { subject, html };
+ };
+
+ buildHardwareNotificationMessage = (alerts) => {
+ return alerts.map((alert) => alert).join("\n");
+ };
+}
+
+export default NotificationUtils;
diff --git a/server/validation/joi.js b/server/validation/joi.js
index e684cefa2..448a844f1 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -570,24 +570,15 @@ const createNotificationBodyValidation = joi.object({
"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",
- }),
+ type: joi
+ .string()
+ .valid("email", "webhook", "slack", "discord", "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",
}),
- }),
});
//****************************************