refactor(backend): move notifications update endpoint to monitors route and handle security feedback

This commit is contained in:
ashu130698 committed 2026-03-10 22:57:55 +05:30
1 parent 5578fdf17f
commit 0fc338db2f
9 files changed
+78 -42

No files matched your search

+2 -2
View File
@@ -31,7 +31,7 @@ export interface InitializedControllers {
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
return {
authController: new AuthController(services.userService),
monitorController: new MonitorController(services.monitorService),
monitorController: new MonitorController(services.monitorService, services.notificationsService),
settingsController: new SettingsController(services.settingsService, services.emailService),
checkController: new CheckController(services.checkService),
geoCheckController: new GeoCheckController(services.geoChecksService),
@@ -40,7 +40,7 @@ export const initializeControllers = (services: InitializedServices): Initialize
queueController: new QueueController(services.jobQueue),
logController: new LogController(services.logger),
statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService),
notificationController: new NotificationController(services.notificationsService, services.monitorsRepository),
notificationController: new NotificationController(services.notificationsService),
diagnosticController: new DiagnosticController(services.diagnosticService),
incidentController: new IncidentController(services.incidentService),
};
+41 -3
View File
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
@@ -26,7 +26,7 @@ import {
requireUserId,
} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorService } from "@/service/index.js";
import { IMonitorService, INotificationsService } from "@/service/index.js";
import { GeoContinent } from "@/types/geoCheck.js";
const SERVICE_NAME = "monitorController";
@@ -34,9 +34,11 @@ class MonitorController {
static SERVICE_NAME = SERVICE_NAME;
private monitorService: IMonitorService;
private notificationsService: INotificationsService;
constructor(monitorService: IMonitorService) {
constructor(monitorService: IMonitorService, notificationsService: INotificationsService) {
this.monitorService = monitorService;
this.notificationsService = notificationsService;
}
get serviceName() {
@@ -439,6 +441,42 @@ class MonitorController {
next(error);
}
};
updateNotifications = async (req: Request, res: Response, next: NextFunction) => {
try {
updateNotificationsValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId);
const { monitorIds, notificationIds, action } = req.body;
// Verify all requested notification IDs actually belong to this team
const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId);
const validNotificationIds = teamNotifications.map((n) => n.id);
const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id));
if (invalidIds.length > 0) {
throw new AppError({
message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`,
status: 403
});
}
const modifiedCount = await this.monitorService.updateNotifications({
teamId,
monitorIds,
notificationIds,
action
});
return res.status(200).json({
success: true,
msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`,
data: { modifiedCount },
});
} catch (error) {
next(error);
}
};
}
export default MonitorController;
@@ -1,9 +1,8 @@
import { Request, Response, NextFunction } from "express";
import { Notification } from "@/types/index.js";
import { createNotificationBodyValidation, bulkEditNotificationBodyValidation } from "@/validation/notificationValidation.js";
import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorsRepository } from "@/repositories/index.js";
import { INotificationsService } from "@/service/index.js";
import { requireTeamId } from "./controllerUtils.js";
@@ -12,10 +11,8 @@ const SERVICE_NAME = "NotificationController";
class NotificationController {
static SERVICE_NAME = SERVICE_NAME;
private notificationsService: INotificationsService;
private monitorsRepository: IMonitorsRepository;
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
constructor(notificationsService: INotificationsService) {
this.notificationsService = notificationsService;
this.monitorsRepository = monitorsRepository;
}
get serviceName() {
@@ -150,25 +147,6 @@ class NotificationController {
}
};
// Handler for bulk editing monitor notification associations
bulkEditMonitorNotifications = async (req: Request, res: Response, next: NextFunction) => {
try {
bulkEditNotificationBodyValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId);
const { monitorIds, notificationIds, action } = req.body;
const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId);
return res.status(200).json({
success: true,
msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`,
data: { modifiedCount },
});
} catch (error) {
next(error);
}
};
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
try {
@@ -178,9 +156,9 @@ class NotificationController {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
// Removed logic since that function shouldn't be here, testing notifications is separate from this controller.
const notifications: string[] = []; // Fix TS definition error temporarily.
const notifications = monitor.notifications;
if (notifications.length === 0) {
throw new AppError({ message: "No notifications", status: 400 });
}
@@ -305,23 +305,25 @@ class MongoMonitorsRepository implements IMonitorsRepository {
teamId: string
): Promise<number> => {
let objectIds;
let notificationObjectIds;
try {
objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
notificationObjectIds = notificationIds.map((id) => new mongoose.Types.ObjectId(id));
} catch {
throw new AppError({ message: "One or more monitor IDs are invalid", status: 400 });
throw new AppError({ message: "One or more monitor or notification IDs are invalid", status: 400 });
}
const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) };
let update;
switch (action) {
case "add":
update = { $addToSet: { notifications: { $each: notificationIds } } };
update = { $addToSet: { notifications: { $each: notificationObjectIds } } };
break;
case "remove":
update = { $pull: { notifications: { $in: notificationIds } } };
update = { $pull: { notifications: { $in: notificationObjectIds } } };
break;
case "set":
update = { $set: { notifications: notificationIds } };
update = { $set: { notifications: notificationObjectIds } };
break;
default:
throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
+1
View File
@@ -42,6 +42,7 @@ class MonitorRoutes {
});
// General monitor CRUD routes
this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications);
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
-1
View File
@@ -17,7 +17,6 @@ class NotificationRoutes {
this.router.get("/team", this.notificationController.getNotificationsByTeamId);
this.router.patch("/", this.notificationController.bulkEditMonitorNotifications);
this.router.get("/:id", this.notificationController.getNotificationById);
this.router.delete("/:id", this.notificationController.deleteNotification);
@@ -72,6 +72,9 @@ export interface IMonitorService {
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
deleteAllMonitors(args: { teamId: string }): Promise<number>;
// notifications
updateNotifications(args: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise<number>;
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
@@ -442,6 +445,18 @@ export class MonitorService implements IMonitorService {
return editedMonitor;
};
updateNotifications = async ({ teamId, monitorIds, notificationIds, action }: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise<number> => {
const modifiedCount = await this.monitorsRepository.updateNotifications(monitorIds, notificationIds, action, teamId);
// If notifications were updated, we should update the jobs in the queue
if (modifiedCount > 0) {
const monitors = await this.monitorsRepository.findByIds(monitorIds);
await Promise.all(monitors.map(monitor => this.jobQueue.updateJob(monitor)));
}
return modifiedCount;
};
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId);
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor);
+8 -5
View File
@@ -135,13 +135,13 @@ export class UserService implements IUserService {
user.password = this.hashPassword(user.password);
}
const newUser = await this.usersRepository.create(user, file);
const newUser = await this.usersRepository.create(user as User, file);
this.logger.debug({
message: "New user created",
service: SERVICE_NAME,
method: "registerUser",
details: newUser.id,
details: { id: newUser.id },
});
delete newUser.profileImage;
@@ -158,7 +158,7 @@ export class UserService implements IUserService {
if (!html) {
throw new Error("Failed to build welcome email HTML");
}
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => {
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html as string).catch((error: unknown) => {
this.logger.warn({
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
@@ -199,13 +199,13 @@ export class UserService implements IUserService {
userData.password = this.hashPassword(userData.password);
}
const newUser = await this.usersRepository.create(userData, file);
const newUser = await this.usersRepository.create(userData as User, file);
this.logger.debug({
message: "New user created by superadmin",
service: SERVICE_NAME,
method: "createUser",
details: newUser.id,
details: { id: newUser.id },
});
newUser.profileImage = undefined;
@@ -280,6 +280,9 @@ export class UserService implements IUserService {
email,
url,
});
if (!html) {
throw new AppError({ message: "Failed to generate password reset email", status: 500 });
}
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
return msgId;
};
@@ -77,7 +77,7 @@ export const sendTestEmailBodyValidation = z.object({
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
});
export const bulkEditNotificationBodyValidation = z.object({
export const updateNotificationsValidation = z.object({
monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required"),
notificationIds: z.array(z.string()),
action: z.enum(["add", "remove", "set"] as const),