status page validation

This commit is contained in:
Alex Holliday committed 2026-03-02 19:25:34 +00:00
1 parent 8236b02034
commit 16cf23baf1
6 files changed
+70 -81

No files matched your search

+10 -6
View File
@@ -33,8 +33,10 @@ class StatusPageController {
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
try {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
createStatusPageBodyValidation.parse(req.body);
if (req.file) {
imageValidation.parse(req.file);
}
const teamId = requireTeamId(req?.user?.teamId);
const userId = requireUserId(req?.user?.id);
@@ -52,8 +54,10 @@ class StatusPageController {
updateStatusPage = async (req: Request, res: Response, next: NextFunction) => {
try {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
createStatusPageBodyValidation.parse(req.body);
if (req.file) {
imageValidation.parse(req.file);
}
const teamId = requireTeamId(req?.user?.teamId);
const statusPageId = req.params.id as string;
if (!statusPageId) {
@@ -75,8 +79,8 @@ class StatusPageController {
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
getStatusPageParamValidation.parse(req.params);
getStatusPageQueryValidation.parse(req.query);
if (!req.params.url) {
throw new AppError({ message: "Status page URL is required", status: 400 });
+4 -16
View File
@@ -1,23 +1,11 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/types/geoCheck.js";
//****************************************
// Check Validations
//****************************************
export const ackCheckBodyValidation = z.object({
ack: z.coerce.boolean().optional(),
});
export const ackAllChecksParamValidation = z.object({
monitorId: z.string().optional(),
path: z.enum(["monitor", "team"]),
});
export const ackAllChecksBodyValidation = z.object({
ack: z.coerce.boolean().optional(),
});
export const getChecksParamValidation = z.object({
monitorId: z.string().min(1, "Monitor ID is required"),
});
@@ -28,10 +16,10 @@ export const getChecksQueryValidation = z.object({
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
ack: z.coerce.boolean().optional(),
ack: booleanCoercion.optional(),
page: z.coerce.number().optional(),
rowsPerPage: z.coerce.number().optional(),
status: z.coerce.boolean().optional(),
status: booleanCoercion.optional(),
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
});
@@ -40,7 +28,7 @@ export const getTeamChecksQueryValidation = z.object({
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
filter: z.enum(["all", "up", "down", "resolve"]).optional(),
ack: z.coerce.boolean().optional(),
ack: booleanCoercion.optional(),
page: z.coerce.number().optional(),
rowsPerPage: z.coerce.number().optional(),
});
@@ -1,4 +1,5 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
//****************************************
// Maintenance Window Validations
@@ -21,7 +22,7 @@ export const getMaintenanceWindowByIdParamValidation = z.object({
});
export const getMaintenanceWindowsByTeamIdQueryValidation = z.object({
active: z.coerce.boolean().optional(),
active: booleanCoercion.optional(),
page: z.coerce.number().optional(),
rowsPerPage: z.coerce.number().optional(),
field: z.string().optional(),
+4 -3
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/types/geoCheck.js";
export const getMonitorByIdParamValidation = z.object({
@@ -6,12 +7,12 @@ export const getMonitorByIdParamValidation = z.object({
});
export const getMonitorByIdQueryValidation = z.object({
status: z.coerce.boolean().optional(),
status: booleanCoercion.optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
numToDisplay: z.coerce.number().optional(),
normalize: z.coerce.boolean().optional(),
normalize: booleanCoercion.optional(),
continent: z.enum(GeoContinents).optional(),
});
@@ -40,7 +41,7 @@ export const getMonitorsWithChecksQueryValidation = z.object({
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
])
.optional(),
explain: z.coerce.boolean().optional(),
explain: booleanCoercion.optional(),
});
export const getCertificateParamValidation = z.object({
+10
View File
@@ -29,6 +29,16 @@ export const lowercaseEmailValidation = z
message: "Email must be in lowercase",
});
/**
* Helper to properly coerce boolean from strings (handles "true"/"false" strings)
* Needed for query parameters and form data where Express sends strings
*/
export const booleanCoercion = z.preprocess((val) => {
if (val === "true" || val === true) return true;
if (val === "false" || val === false) return false;
return val; // Let Zod validation handle invalid values
}, z.boolean());
/**
* Custom validator for role-based authorization
*/
+40 -55
View File
@@ -1,68 +1,53 @@
import joi from "joi";
import { z } from "zod";
import { booleanCoercion } from "./shared.js";
//****************************************
// Status Page Validations
//****************************************
export const getStatusPageParamValidation = joi.object({
url: joi.string().required(),
export const getStatusPageParamValidation = z.object({
url: z.string().min(1, "URL is required"),
});
export const getStatusPageQueryValidation = joi.object({
type: joi.string().valid("uptime").required(),
timeFrame: joi.number().optional(),
export const getStatusPageQueryValidation = z.object({
type: z.literal("uptime"),
timeFrame: z.coerce.number().optional(),
});
export const createStatusPageBodyValidation = joi.object({
type: joi.string().valid("uptime").required(),
companyName: joi.string().required(),
url: joi
.string()
.pattern(/^[a-zA-Z0-9_-]+$/)
.required()
.messages({
"string.pattern.base": "URL can only contain letters, numbers, underscores, and hyphens",
}),
timezone: joi.string().optional(),
color: joi.string().optional(),
monitors: joi
.array()
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
.required()
.messages({
"string.pattern.base": "Must be a valid monitor ID",
"array.base": "Monitors must be an array",
"array.empty": "At least one monitor is required",
"any.required": "Monitors are required",
}),
subMonitors: joi
.array()
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
.optional(),
deleteSubmonitors: joi.boolean().optional(),
isPublished: joi.boolean(),
showCharts: joi.boolean().optional(),
showUptimePercentage: joi.boolean(),
showAdminLoginLink: joi.boolean().optional(),
removeLogo: joi.string().valid("true", "false").optional(),
});
export const imageValidation = joi
export const createStatusPageBodyValidation = z
.object({
fieldname: joi.string().required(),
originalname: joi.string().required(),
encoding: joi.string().required(),
mimetype: joi.string().valid("image/jpeg", "image/png", "image/jpg").required().messages({
"string.valid": "File must be a valid image (jpeg, jpg, or png)",
type: z.literal("uptime"),
companyName: z.string().min(1, "Company name is required"),
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
message: "URL can only contain letters, numbers, underscores, and hyphens",
}),
size: joi.number().max(3145728).required().messages({
"number.max": "File size must be less than 3MB",
}),
buffer: joi.binary().required(),
destination: joi.string(),
filename: joi.string(),
path: joi.string(),
timezone: z.string().optional(),
color: z.string().optional(),
monitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/, "Must be a valid monitor ID")).min(1, "At least one monitor is required"),
subMonitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/)).optional(),
deleteSubmonitors: z.boolean().optional(),
isPublished: booleanCoercion,
showCharts: booleanCoercion.optional(),
showUptimePercentage: booleanCoercion,
showAdminLoginLink: booleanCoercion.optional(),
removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(),
})
.messages({
"any.required": "Image file is required",
.strip();
export const imageValidation = z
.object({
fieldname: z.string().min(1, "Field name is required"),
originalname: z.string().min(1, "Original name is required"),
encoding: z.string().min(1, "Encoding is required"),
mimetype: z.enum(["image/jpeg", "image/png", "image/jpg"], {
message: "File must be a valid image (jpeg, jpg, or png)",
}),
size: z.number().max(3145728, "File size must be less than 3MB"),
buffer: z.instanceof(Buffer, { message: "Buffer is required" }),
destination: z.string().optional(),
filename: z.string().optional(),
path: z.string().optional(),
})
.refine((data) => data.buffer, {
message: "Image file is required",
});