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