Merge pull request #3410 from bluewave-labs/fix/monitor-controller-validation

fix: validation in monitor controller
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-03-11 13:15:41 -07:00
commit 01f2aa294e
5 files changed
+145 -146

No files matched your search

+74 -117
View File
@@ -3,7 +3,6 @@ import { Request, Response, NextFunction } from "express";
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation,
createMonitorBodyValidation,
@@ -12,22 +11,14 @@ import {
getCertificateParamValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
getUptimeDetailsByIdParamValidation,
getUptimeDetailsByIdQueryValidation,
importMonitorsBodyValidation,
} from "@/validation/monitorValidation.js";
import sslChecker from "ssl-checker";
import {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
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 {
@@ -45,10 +36,9 @@ class MonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try {
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 validatedParams = getCertificateParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId);
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId });
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
return res.status(200).json({
@@ -65,10 +55,14 @@ 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 teamId = requireTeamId(req?.user?.teamId);
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({
teamId,
@@ -88,12 +82,12 @@ 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 teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId;
const dateRange = validatedQuery.dateRange || "recent";
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getHardwareDetailsById({
teamId,
@@ -112,12 +106,12 @@ 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 teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId;
const dateRange = validatedQuery.dateRange || "recent";
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getPageSpeedDetailsById({
teamId,
@@ -137,18 +131,14 @@ 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 continents = continentParam
? Array.isArray(continentParam)
? (continentParam as GeoContinent[])
: [continentParam as GeoContinent]
: undefined;
const teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId;
const dateRange = validatedQuery.dateRange || "recent";
const continentParam = validatedQuery.continent;
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined;
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getGeoChecksByMonitorId({
teamId,
@@ -169,11 +159,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 teamId = requireTeamId(req.user?.teamId);
const monitorId = validatedParams.monitorId;
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
@@ -189,12 +178,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 userId = requireUserId(req.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,
@@ -208,14 +197,10 @@ class MonitorController {
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
try {
const teamId = requireTeamId(req?.user?.teamId);
const userId = requireUserId(req?.user?.id);
const { monitors } = req.body;
if (!monitors || !Array.isArray(monitors)) {
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
}
const teamId = requireTeamId(req.user?.teamId);
const userId = requireUserId(req.user?.id);
const validatedBody = importMonitorsBodyValidation.parse(req.body);
const monitors = validatedBody.monitors;
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
@@ -231,9 +216,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 teamId = requireTeamId(req?.user?.teamId);
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId);
const monitorId = validatedParams.monitorId;
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -249,8 +234,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({
@@ -264,12 +248,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 teamId = requireTeamId(req?.user?.teamId);
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,10 +267,10 @@ 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 teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId;
const teamId = requireTeamId(req.user?.teamId);
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -302,8 +286,8 @@ class MonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = requireString(req?.user?.id, "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({
@@ -316,32 +300,13 @@ 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 });
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 {
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 teamId = requireTeamId(req.user?.teamId);
const type = validatedQuery.type;
const filter = validatedQuery.filter;
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
@@ -357,17 +322,17 @@ 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 teamId = requireTeamId(req?.user?.teamId);
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({
teamId,
@@ -392,11 +357,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 +385,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({
-1
View File
@@ -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
+3 -18
View File
@@ -29,7 +29,7 @@ export interface IMonitorService {
readonly serviceName: string;
// create
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
@@ -64,7 +64,7 @@ export interface IMonitorService {
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
// update
editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
editMonitor(args: { teamId: string; monitorId: string; body: Partial<Monitor> }): Promise<Monitor>;
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
// delete
@@ -72,7 +72,6 @@ export interface IMonitorService {
deleteAllMonitors(args: { teamId: string }): Promise<number>;
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
}
@@ -430,7 +429,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<Monitor> }) => {
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
await this.jobQueue.updateJob(editedMonitor);
return editedMonitor;
@@ -509,20 +508,6 @@ export class MonitorService implements IMonitorService {
return deletedCount;
};
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
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<any[]> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
+2 -1
View File
@@ -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;
+66 -9
View File
@@ -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"),
@@ -13,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({});
@@ -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(),
@@ -112,6 +113,62 @@ 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"),
});