Merge pull request #3376 from ashu130698/feat/bulk-edit-notifications
feat: add bulk edit monitor notifications endpoint
This commit is contained in:
10 files changed
+131
-30
No files matched your search
Generated
+3
-22
@@ -83,7 +83,6 @@
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -354,7 +353,6 @@
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.14.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -392,7 +390,6 @@
|
||||
"node_modules/@emotion/styled": {
|
||||
"version": "11.14.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -1162,7 +1159,6 @@
|
||||
"node_modules/@mui/material": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/core-downloads-tracker": "^7.3.7",
|
||||
@@ -1267,7 +1263,6 @@
|
||||
"node_modules/@mui/system": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/private-theming": "^7.3.7",
|
||||
@@ -2308,7 +2303,6 @@
|
||||
"version": "24.5.2",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
@@ -2324,7 +2318,6 @@
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2446,7 +2439,6 @@
|
||||
"version": "8.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2771,7 +2763,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -3203,8 +3194,7 @@
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
@@ -3586,7 +3576,6 @@
|
||||
"version": "8.57.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -4331,7 +4320,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -5115,7 +5103,6 @@
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
||||
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
@@ -5763,7 +5750,6 @@
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -5775,7 +5761,6 @@
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.71.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -5975,8 +5960,7 @@
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-persist": {
|
||||
"version": "6.0.0",
|
||||
@@ -6122,11 +6106,10 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -6915,7 +6898,6 @@
|
||||
"version": "5.9.3",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7051,7 +7033,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
||||
import {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
} from "@/validation/monitorValidation.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService, INotificationsService } from "@/service/index.js";
|
||||
import { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
@@ -40,14 +42,17 @@ export interface IMonitorController {
|
||||
exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||
getAllGames: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||
getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||
updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||
}
|
||||
class MonitorController implements IMonitorController {
|
||||
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() {
|
||||
@@ -412,6 +417,42 @@ class MonitorController implements IMonitorController {
|
||||
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;
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
testAllNotificationsBodyValidation,
|
||||
} from "@/validation/notificationValidation.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { INotificationsService } from "@/service/index.js";
|
||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
@@ -142,8 +142,8 @@ class NotificationController implements INotificationController {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
||||
const notifications = monitor.notifications || [];
|
||||
|
||||
const notifications = monitor.notifications;
|
||||
if (notifications.length === 0) {
|
||||
throw new AppError({ message: "No notifications", status: 400 });
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface IMonitorsRepository {
|
||||
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
|
||||
findGroupsByTeamId(teamId: string): Promise<string[]>;
|
||||
removeNotificationFromMonitors(notificationId: string): Promise<void>;
|
||||
updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise<number>;
|
||||
deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>;
|
||||
findAllMonitorIds(): Promise<string[]>;
|
||||
}
|
||||
@@ -296,6 +296,41 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
|
||||
};
|
||||
|
||||
updateNotifications = async (
|
||||
teamId: string,
|
||||
monitorIds: string[],
|
||||
notificationIds: string[],
|
||||
action: "add" | "remove" | "set"
|
||||
): 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 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: notificationObjectIds } } };
|
||||
break;
|
||||
case "remove":
|
||||
update = { $pull: { notifications: { $in: notificationObjectIds } } };
|
||||
break;
|
||||
case "set":
|
||||
update = { $set: { notifications: notificationObjectIds } };
|
||||
break;
|
||||
default:
|
||||
throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
|
||||
}
|
||||
|
||||
const result = await MonitorModel.updateMany(filter, update);
|
||||
return result.modifiedCount;
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
|
||||
if (!documents?.length) {
|
||||
return [];
|
||||
|
||||
@@ -38,6 +38,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);
|
||||
|
||||
|
||||
@@ -79,6 +79,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
|
||||
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
||||
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: ImportedMonitor[] }): Promise<{ imported: number; errors: string[] }>;
|
||||
@@ -439,6 +442,28 @@ 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(teamId, monitorIds, notificationIds, action);
|
||||
|
||||
// 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);
|
||||
if (monitor.isActive) {
|
||||
|
||||
@@ -143,7 +143,7 @@ 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",
|
||||
@@ -207,7 +207,7 @@ 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",
|
||||
|
||||
@@ -98,3 +98,20 @@ export const sendTestEmailBodyValidation = z.object({
|
||||
systemEmailRejectUnauthorized: z.boolean().optional(),
|
||||
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
|
||||
});
|
||||
|
||||
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),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.action !== "set" && data.notificationIds.length === 0) return false;
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Notification IDs cannot be empty unless action is 'set'",
|
||||
path: ["notificationIds"],
|
||||
}
|
||||
);
|
||||
Reference in new issue
Block a user