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"), });