From 545926cfcf1bd0c829a3de4cd7c03eb3ec059689 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 12:41:27 -0700 Subject: [PATCH 1/4] fix validation in monitor controller --- server/src/controllers/monitorController.ts | 124 ++++++++---------- server/src/service/business/monitorService.ts | 6 +- server/src/types/monitor.ts | 3 +- server/src/validation/monitorValidation.ts | 77 +++++++++-- 4 files changed, 132 insertions(+), 78 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 1d222e6e2..dc78641c4 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -12,6 +12,10 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, + getUptimeDetailsByIdParamValidation, + getUptimeDetailsByIdQueryValidation, + importMonitorsBodyValidation, + sendTestEmailBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; import { @@ -45,10 +49,9 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { - getCertificateParamValidation.parse(req.params); + const validatedParams = getCertificateParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); - const monitorId = requireString(req.params?.monitorId, "Monitor ID"); - const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); return res.status(200).json({ @@ -65,9 +68,13 @@ class MonitorController { getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const normalize = optionalBoolean(req?.query?.normalize, "normalize"); + const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange; + const normalize = validatedQuery.normalize; + const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getUptimeDetailsById({ @@ -88,11 +95,11 @@ class MonitorController { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - getHardwareDetailsByIdParamValidation.parse(req.params); - getHardwareDetailsByIdQueryValidation.parse(req.query); + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent"; + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getHardwareDetailsById({ @@ -112,11 +119,11 @@ class MonitorController { }; getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - getHardwareDetailsByIdParamValidation.parse(req.params); - getHardwareDetailsByIdQueryValidation.parse(req.query); + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getPageSpeedDetailsById({ @@ -137,12 +144,12 @@ class MonitorController { getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - getMonitorByIdQueryValidation.parse(req.query); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const continentParam = req?.query?.continent; + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const continentParam = validatedQuery.continent; const continents = continentParam ? Array.isArray(continentParam) ? (continentParam as GeoContinent[]) @@ -169,11 +176,10 @@ class MonitorController { getMonitorById = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - getMonitorByIdQueryValidation.parse(req.query); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const monitorId = validatedParams.monitorId; const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -189,12 +195,12 @@ class MonitorController { createMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - createMonitorBodyValidation.parse(req.body); + const validatedBody = createMonitorBodyValidation.parse(req.body); const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); - const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); + const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); return res.status(200).json({ success: true, @@ -210,8 +216,8 @@ class MonitorController { try { const teamId = requireTeamId(req?.user?.teamId); const userId = requireUserId(req?.user?.id); - - const { monitors } = req.body; + const validatedBody = importMonitorsBodyValidation.parse(req.body); + const monitors = validatedBody.monitors; if (!monitors || !Array.isArray(monitors)) { throw new AppError({ message: "Invalid request: monitors array is required", status: 400 }); @@ -231,9 +237,9 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -250,7 +256,6 @@ class MonitorController { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req?.user?.teamId); - const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); return res.status(200).json({ @@ -264,12 +269,12 @@ class MonitorController { editMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - editMonitorBodyValidation.parse(req.body); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedBody = editMonitorBodyValidation.parse(req.body); + const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId); - const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); return res.status(200).json({ success: true, @@ -283,9 +288,9 @@ class MonitorController { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - pauseMonitorParamValidation.parse(req.params); + const validatedParams = pauseMonitorParamValidation.parse(req.params); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -302,7 +307,7 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const id = requireString(req?.user?.id, "User ID"); + const id = requireUserId(req?.user?.id); const teamId = requireTeamId(req?.user?.teamId); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); @@ -318,12 +323,8 @@ class MonitorController { sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { try { - const { to } = req.body; - if (!to || typeof to !== "string") { - throw new AppError({ message: "Invalid 'to' email address", status: 400 }); - } - - const messageId = await this.monitorService.sendTestEmail({ to }); + const validatedBody = sendTestEmailBodyValidation.parse(req.body); + const messageId = await this.monitorService.sendTestEmail({ to: validatedBody.to }); return res.status(200).json({ success: true, msg: "Test email sent successfully", @@ -336,12 +337,11 @@ class MonitorController { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorsByTeamIdParamValidation.parse(req.params); - getMonitorsByTeamIdQueryValidation.parse(req.query); + const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); - const type = parseMonitorTypeFilter(req.query?.type); - const filter = optionalString(req.query?.filter, "filter"); + const type = validatedQuery.type; + const filter = validatedQuery.filter; const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); @@ -357,16 +357,16 @@ class MonitorController { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorsByTeamIdParamValidation.parse(req.params); - getMonitorsWithChecksQueryValidation.parse(req.query); - const explain = optionalBoolean(req?.query?.explain, "explain"); - const limit = optionalNumber(req?.query?.limit, "limit"); - const page = optionalNumber(req?.query?.page, "page"); - const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage"); - const filter = optionalString(req?.query?.filter, "filter"); - const field = optionalString(req?.query?.field, "field"); - const order = parseSortOrder(req?.query?.order); - const type = parseMonitorTypeFilter(req?.query?.type); + const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); + + const explain = validatedQuery.explain; + const limit = validatedQuery.limit; + const page = validatedQuery.page; + const rowsPerPage = validatedQuery.rowsPerPage; + const filter = validatedQuery.filter; + const field = validatedQuery.field; + const order = validatedQuery.order; + const type = validatedQuery.type; const teamId = requireTeamId(req?.user?.teamId); const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ @@ -392,11 +392,7 @@ class MonitorController { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - + const teamId = requireTeamId(req?.user?.teamId); const json = await this.monitorService.exportMonitorsToJSON({ teamId }); return res.status(200).json({ @@ -424,11 +420,7 @@ class MonitorController { getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - + const teamId = requireTeamId(req.user?.teamId); const groups = await this.monitorService.getGroupsByTeamId({ teamId }); return res.status(200).json({ diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index a50598256..eea1b03d0 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -29,7 +29,7 @@ export interface IMonitorService { readonly serviceName: string; // create - createMonitor(teamId: string, userId: string, body: Monitor): Promise; + createMonitor(teamId: string, userId: string, body: Partial): Promise; createMonitors(monitors: Array, userId: string, teamId: string): Promise; addDemoMonitors(args: { userId: string; teamId: string }): Promise; @@ -64,7 +64,7 @@ export interface IMonitorService { getGroupsByTeamId(args: { teamId: string }): Promise; // update - editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; + editMonitor(args: { teamId: string; monitorId: string; body: Partial }): Promise; pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete @@ -430,7 +430,7 @@ export class MonitorService implements IMonitorService { return groups; }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial }) => { const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.jobQueue.updateJob(editedMonitor); return editedMonitor; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 89740162b..bac2313f5 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -12,7 +12,8 @@ export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupporte export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; -export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; +export const MonitorMatchMethods = ["equal", "include", "regex"] as const; +export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | ""; export interface Monitor { id: string; diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 75ffccd19..64d320653 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { GeoContinents } from "@/types/geoCheck.js"; +import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; export const getMonitorByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -25,14 +26,14 @@ export const getMonitorsByTeamIdQueryValidation = z.object({ z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])), ]) .optional(), - filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + filter: z.union([z.string(), z.literal("")]).optional(), }); export const getMonitorsWithChecksQueryValidation = z.object({ limit: z.coerce.number().int().min(1).max(100).optional(), page: z.coerce.number().int().min(0).optional(), rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), - filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + filter: z.union([z.string(), z.literal("")]).optional(), field: z.string().optional(), order: z.enum(["asc", "desc"]).optional(), type: z @@ -51,8 +52,8 @@ export const getCertificateParamValidation = z.object({ export const createMonitorBodyValidation = z.object({ _id: z.string().optional(), name: z.string().min(1, "Name is required"), - description: z.union([z.string(), z.null(), z.literal("")]).optional(), - type: z.string().min(1, "Type is required"), + description: z.union([z.string(), z.literal("")]).optional(), + type: z.enum(MonitorTypes, "Invalid monitor type"), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), url: z.string().min(1, "URL is required"), @@ -69,7 +70,7 @@ export const createMonitorBodyValidation = z.object({ secret: z.string().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), - matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(), grpcServiceName: z.union([z.string(), z.literal("")]).default(""), selectedDisks: z.array(z.string()).optional(), @@ -81,11 +82,11 @@ export const createMonitorBodyValidation = z.object({ export const editMonitorBodyValidation = z.object({ name: z.string().optional(), - type: z.string().optional(), + type: z.enum(MonitorTypes).optional(), url: z.string().optional(), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), - description: z.union([z.string(), z.null(), z.literal("")]).optional(), + description: z.union([z.string(), z.literal("")]).optional(), interval: z.number().optional(), notifications: z.array(z.string()).optional(), secret: z.string().optional(), @@ -93,7 +94,7 @@ export const editMonitorBodyValidation = z.object({ useAdvancedMatching: z.boolean().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), - matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), port: z.number().min(1).max(65535).optional(), cpuAlertThreshold: z.number().optional(), memoryAlertThreshold: z.number().optional(), @@ -108,10 +109,70 @@ export const editMonitorBodyValidation = z.object({ geoCheckInterval: z.number().min(300000).optional(), }); +export const sendTestEmailBodyValidation = z.object({ + to: z.string().email("Invalid 'to' email address"), +}); + export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); +export const getUptimeDetailsByIdParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getUptimeDetailsByIdQueryValidation = z.object({ + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), + normalize: booleanCoercion.optional(), +}); + +const importedMonitorSchema = z.object({ + id: z.string().optional(), + userId: z.string().optional(), + teamId: z.string().optional(), + name: z.string().min(1, "Name is required"), + description: z.union([z.string(), z.literal("")]).optional(), + status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).optional(), + statusWindow: z.array(z.boolean()).optional(), + statusWindowSize: z.number().optional(), + statusWindowThreshold: z.number().optional(), + type: z.enum(MonitorTypes, "Invalid monitor type"), + ignoreTlsErrors: z.boolean().optional(), + useAdvancedMatching: z.boolean().optional(), + jsonPath: z.union([z.string(), z.literal("")]).optional(), + expectedValue: z.union([z.string(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), + url: z.string().min(1, "URL is required"), + port: z.number().optional(), + isActive: z.boolean().optional(), + interval: z.number().optional(), + uptimePercentage: z.number().optional(), + notifications: z.array(z.string()).optional(), + secret: z.string().optional(), + cpuAlertThreshold: z.number().optional(), + cpuAlertCounter: z.number().optional(), + memoryAlertThreshold: z.number().optional(), + memoryAlertCounter: z.number().optional(), + diskAlertThreshold: z.number().optional(), + diskAlertCounter: z.number().optional(), + tempAlertThreshold: z.number().optional(), + tempAlertCounter: z.number().optional(), + selectedDisks: z.array(z.string()).optional(), + gameId: z.union([z.string(), z.literal("")]).optional(), + grpcServiceName: z.union([z.string(), z.literal("")]).optional(), + group: z.union([z.string(), z.null()]).optional(), + geoCheckEnabled: z.boolean().optional(), + geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), + geoCheckInterval: z.number().optional(), + recentChecks: z.array(z.unknown()).optional(), + createdAt: z.string().optional(), + updatedAt: z.string().optional(), +}); + +export const importMonitorsBodyValidation = z.object({ + monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"), +}); + export const getHardwareDetailsByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); From ff90953f17534920f86fbdcf72cbfffb1b28ffda Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 12:57:27 -0700 Subject: [PATCH 2/4] cleanup and tighten validation --- server/src/controllers/monitorController.ts | 57 ++++++++------------- server/src/validation/monitorValidation.ts | 2 +- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index dc78641c4..7310e7656 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -3,7 +3,6 @@ import { Request, Response, NextFunction } from "express"; import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, getMonitorsWithChecksQueryValidation, createMonitorBodyValidation, @@ -18,17 +17,7 @@ import { sendTestEmailBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { - fetchMonitorCertificate, - requireString, - optionalString, - optionalNumber, - optionalBoolean, - parseMonitorTypeFilter, - parseSortOrder, - requireTeamId, - requireUserId, -} from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireString, requireTeamId, requireUserId } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/service/index.js"; import { GeoContinent } from "@/types/geoCheck.js"; @@ -50,7 +39,7 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = getCertificateParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); @@ -75,7 +64,7 @@ class MonitorController { const dateRange = validatedQuery.dateRange; const normalize = validatedQuery.normalize; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getUptimeDetailsById({ teamId, @@ -100,7 +89,7 @@ class MonitorController { const monitorId = validatedParams.monitorId; const dateRange = validatedQuery.dateRange || "recent"; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getHardwareDetailsById({ teamId, @@ -124,7 +113,7 @@ class MonitorController { const monitorId = validatedParams.monitorId; const dateRange = validatedQuery.dateRange || "recent"; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getPageSpeedDetailsById({ teamId, @@ -150,12 +139,8 @@ class MonitorController { const monitorId = validatedParams.monitorId; const dateRange = validatedQuery.dateRange || "recent"; const continentParam = validatedQuery.continent; - const continents = continentParam - ? Array.isArray(continentParam) - ? (continentParam as GeoContinent[]) - : [continentParam as GeoContinent] - : undefined; - const teamId = requireTeamId(req?.user?.teamId); + const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getGeoChecksByMonitorId({ teamId, @@ -178,7 +163,7 @@ class MonitorController { try { const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitorId = validatedParams.monitorId; const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -197,8 +182,8 @@ class MonitorController { try { const validatedBody = createMonitorBodyValidation.parse(req.body); - const userId = requireString(req?.user?.id, "User ID"); - const teamId = requireTeamId(req?.user?.teamId); + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); @@ -214,8 +199,8 @@ class MonitorController { importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - const userId = requireUserId(req?.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); const validatedBody = importMonitorsBodyValidation.parse(req.body); const monitors = validatedBody.monitors; @@ -238,7 +223,7 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitorId = validatedParams.monitorId; const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -255,7 +240,7 @@ class MonitorController { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); return res.status(200).json({ @@ -272,7 +257,7 @@ class MonitorController { const validatedParams = getMonitorByIdParamValidation.parse(req.params); const validatedBody = editMonitorBodyValidation.parse(req.body); const monitorId = validatedParams.monitorId; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); @@ -291,7 +276,7 @@ class MonitorController { const validatedParams = pauseMonitorParamValidation.parse(req.params); const monitorId = validatedParams.monitorId; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -307,8 +292,8 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const id = requireUserId(req?.user?.id); - const teamId = requireTeamId(req?.user?.teamId); + const id = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); return res.status(200).json({ @@ -339,7 +324,7 @@ class MonitorController { try { const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const type = validatedQuery.type; const filter = validatedQuery.filter; @@ -367,7 +352,7 @@ class MonitorController { const field = validatedQuery.field; const order = validatedQuery.order; const type = validatedQuery.type; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ teamId, @@ -392,7 +377,7 @@ class MonitorController { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const json = await this.monitorService.exportMonitorsToJSON({ teamId }); return res.status(200).json({ diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 64d320653..1083dd5d1 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -14,7 +14,7 @@ export const getMonitorByIdQueryValidation = z.object({ dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), numToDisplay: z.coerce.number().optional(), normalize: booleanCoercion.optional(), - continent: z.enum(GeoContinents).optional(), + continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), }); export const getMonitorsByTeamIdParamValidation = z.object({}); From 20ff76df9db77f355638ca35cdb43f5b47d31a17 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:07:56 -0700 Subject: [PATCH 3/4] remove unsued route/validation --- server/src/controllers/monitorController.ts | 18 +----------------- server/src/routes/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 15 --------------- server/src/validation/monitorValidation.ts | 4 ---- 4 files changed, 1 insertion(+), 37 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 7310e7656..41d6d95bd 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -14,13 +14,11 @@ import { getUptimeDetailsByIdParamValidation, getUptimeDetailsByIdQueryValidation, importMonitorsBodyValidation, - sendTestEmailBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { fetchMonitorCertificate, requireString, requireTeamId, requireUserId } from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/service/index.js"; -import { GeoContinent } from "@/types/geoCheck.js"; const SERVICE_NAME = "monitorController"; class MonitorController { @@ -306,20 +304,6 @@ class MonitorController { } }; - sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = sendTestEmailBodyValidation.parse(req.body); - const messageId = await this.monitorService.sendTestEmail({ to: validatedBody.to }); - return res.status(200).json({ - success: true, - msg: "Test email sent successfully", - data: { messageId }, - }); - } catch (error) { - next(error); - } - }; - getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); diff --git a/server/src/routes/monitorRoute.ts b/server/src/routes/monitorRoute.ts index 476e421ce..7cd87ee1e 100755 --- a/server/src/routes/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -50,7 +50,6 @@ class MonitorRoutes { this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON); - this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); this.router.get("/games", this.monitorController.getAllGames); // Individual monitor CRUD routes diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index eea1b03d0..936044908 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -72,7 +72,6 @@ export interface IMonitorService { deleteAllMonitors(args: { teamId: string }): Promise; // other - sendTestEmail(args: { to: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>; } @@ -509,20 +508,6 @@ export class MonitorService implements IMonitorService { return deletedCount; }; - sendTestEmail = async ({ to }: { to: string }): Promise => { - const subject = "Test email from Checkmate"; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html); - - if (!messageId) { - throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 }); - } - - return messageId; - }; - exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 1083dd5d1..783f1a029 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -109,10 +109,6 @@ export const editMonitorBodyValidation = z.object({ geoCheckInterval: z.number().min(300000).optional(), }); -export const sendTestEmailBodyValidation = z.object({ - to: z.string().email("Invalid 'to' email address"), -}); - export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); From c156a49cc0cac662a2697e34bb7e8c033e219fb4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:10:09 -0700 Subject: [PATCH 4/4] remove dead check --- server/src/controllers/monitorController.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 41d6d95bd..e66af1306 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -202,10 +202,6 @@ class MonitorController { const validatedBody = importMonitorsBodyValidation.parse(req.body); const monitors = validatedBody.monitors; - if (!monitors || !Array.isArray(monitors)) { - throw new AppError({ message: "Invalid request: monitors array is required", status: 400 }); - } - const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); return res.status(200).json({