validation refactoring

This commit is contained in:
Alex Holliday
2026-03-01 17:35:00 +00:00
parent 0bff26fe47
commit c3a41272a3
23 changed files with 662 additions and 852 deletions
+25 -1
View File
@@ -43,7 +43,8 @@
"ssl-checker": "2.0.10",
"super-simple-scheduler": "1.4.5",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0"
"winston": "^3.13.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -876,6 +877,7 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -1505,6 +1507,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1549,6 +1552,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -4376,6 +4380,7 @@
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -4536,6 +4541,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -5022,6 +5028,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5600,6 +5607,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -6542,6 +6550,7 @@
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssnano-preset-default": "^7.0.9",
"lilconfig": "^3.1.3"
@@ -7314,6 +7323,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7675,6 +7685,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -9323,6 +9334,7 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -12212,6 +12224,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -14436,6 +14449,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -14598,6 +14612,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15402,6 +15417,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+2 -1
View File
@@ -58,7 +58,8 @@
"ssl-checker": "2.0.10",
"super-simple-scheduler": "1.4.5",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0"
"winston": "^3.13.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+19 -16
View File
@@ -6,17 +6,20 @@ import type { UserRole } from "@/types/user.js";
import {
registrationBodyValidation,
loginValidation,
editUserBodyValidation,
createUserBodyValidation,
recoveryValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
} from "@/validation/authValidation.js";
import {
editUserBodyValidation,
createUserBodyValidation,
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
editUserPasswordByIdBodyValidation,
} from "@/validation/joi.js";
} from "@/validation/userValidation.js";
const SERVICE_NAME = "authController";
@@ -40,7 +43,7 @@ class AuthController {
if (newUser?.email) {
newUser.email = newUser.email.toLowerCase();
}
await registrationBodyValidation.validateAsync(newUser);
registrationBodyValidation.parse(newUser);
const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
res.status(200).json({
@@ -59,7 +62,7 @@ class AuthController {
if (userData?.email) {
userData.email = userData.email.toLowerCase();
}
await createUserBodyValidation.validateAsync(userData);
createUserBodyValidation.parse(userData);
const teamId = requireTeamId(req.user?.teamId);
const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
@@ -79,7 +82,7 @@ class AuthController {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await loginValidation.validateAsync(req.body);
loginValidation.parse(req.body);
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
return res.status(200).json({
@@ -97,7 +100,7 @@ class AuthController {
editUser = async (req: Request, res: Response, next: NextFunction) => {
try {
await editUserBodyValidation.validateAsync(req.body);
editUserBodyValidation.parse(req.body);
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
res.status(200).json({
@@ -125,7 +128,7 @@ class AuthController {
requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
try {
await recoveryValidation.validateAsync(req.body);
recoveryValidation.parse(req.body);
const email = req?.body?.email;
const msgId = await this.userService.requestRecovery(email);
return res.status(200).json({
@@ -140,7 +143,7 @@ class AuthController {
validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
try {
await recoveryTokenBodyValidation.validateAsync(req.body);
recoveryTokenBodyValidation.parse(req.body);
await this.userService.validateRecovery(req.body.recoveryToken);
return res.status(200).json({
success: true,
@@ -153,7 +156,7 @@ class AuthController {
resetPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
await newPasswordValidation.validateAsync(req.body);
newPasswordValidation.parse(req.body);
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
return res.status(200).json({
success: true,
@@ -192,7 +195,7 @@ class AuthController {
getUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
await getUserByIdParamValidation.validateAsync(req.params);
getUserByIdParamValidation.parse(req.params);
const userId = req?.params?.userId;
const roles = req?.user?.role;
@@ -223,12 +226,12 @@ class AuthController {
const userId = req.params.userId as string;
const user = { ...req.body };
await editUserByIdParamValidation.validateAsync(req.params);
editUserByIdParamValidation.parse(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user?.id) {
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
editSuperadminUserByIdBodyValidation.parse(req.body);
} else {
await editUserByIdBodyValidation.validateAsync(req.body);
editUserByIdBodyValidation.parse(req.body);
}
await this.userService.editUserById(userId, user);
@@ -246,8 +249,8 @@ class AuthController {
}
const userId = req.params.userId as string;
await editUserByIdParamValidation.validateAsync(req.params);
await editUserPasswordByIdBodyValidation.validateAsync(req.body);
editUserByIdParamValidation.parse(req.params);
editUserPasswordByIdBodyValidation.parse(req.body);
const updatedPassword = req.body.password;
await this.userService.setPasswordByUserId(userId, updatedPassword);
return res.status(200).json({ success: true, msg: "Password reset successfully" });
+1 -1
View File
@@ -10,7 +10,7 @@ import {
ackCheckBodyValidation,
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
} from "@/validation/joi.js";
} from "@/validation/checkValidation.js";
const SERVICE_NAME = "checkController";
+1 -1
View File
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/joi.js";
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
const SERVICE_NAME = "geoCheckController";
+4 -4
View File
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.js";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
const SERVICE_NAME = "inviteController";
@@ -20,7 +20,7 @@ class InviteController {
const userRoles = requireUserRoles(req.user?.role);
const invite = req.body;
invite.teamId = teamId;
await inviteBodyValidation.validateAsync(invite);
inviteBodyValidation.parse(invite);
const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles });
return res.status(200).json({
success: true,
@@ -39,7 +39,7 @@ class InviteController {
const inviteRequest = req.body;
inviteRequest.teamId = teamId;
await inviteBodyValidation.validateAsync(inviteRequest);
inviteBodyValidation.parse(inviteRequest);
const inviteToken = await this.inviteService.sendInviteEmail({
invite: inviteRequest,
@@ -58,7 +58,7 @@ class InviteController {
verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => {
try {
await inviteVerificationBodyValidation.validateAsync(req.body);
inviteVerificationBodyValidation.parse(req.body);
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
return res.status(200).json({
success: true,
@@ -7,7 +7,7 @@ import {
getMaintenanceWindowsByMonitorIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} from "@/validation/joi.js";
} from "@/validation/maintenanceWindowValidation.js";
import { requireTeamId } from "@/controllers/controllerUtils.js";
const SERVICE_NAME = "maintenanceWindowController";
+1 -1
View File
@@ -12,7 +12,7 @@ import {
getCertificateParamValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
} from "@/validation/joi.js";
} from "@/validation/monitorValidation.js";
import sslChecker from "ssl-checker";
import {
fetchMonitorCertificate,
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { Notification } from "@/types/index.js";
import { createNotificationBodyValidation } from "@/validation/joi.js";
import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorsRepository } from "@/repositories/index.js";
import { INotificationsService } from "@/service/index.js";
+3 -2
View File
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { sendTestEmailBodyValidation, updateAppSettingsBodyValidation } from "@/validation/joi.js";
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
import { sendTestEmailBodyValidation } from "@/validation/announcementValidation.js";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "SettingsController";
@@ -68,7 +69,7 @@ class SettingsController {
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
try {
await sendTestEmailBodyValidation.validateAsync(req.body);
sendTestEmailBodyValidation.parse(req.body);
const {
to,
@@ -1,6 +1,11 @@
import { Request, Response, NextFunction } from "express";
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js";
import {
createStatusPageBodyValidation,
getStatusPageParamValidation,
getStatusPageQueryValidation,
imageValidation,
} from "@/validation/statusPageValidation.js";
import { AppError } from "@/utils/AppError.js";
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
import { IStatusPageService } from "@/service/business/statusPageService.js";
@@ -0,0 +1,21 @@
import { z } from "zod";
//****************************************
// Announcement Validations
//****************************************
export const sendTestEmailBodyValidation = z.object({
to: z.string().min(1, "To field is required"),
systemEmailHost: z.string().optional(),
systemEmailPort: z.number().optional(),
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
systemEmailAddress: z.string().optional(),
systemEmailPassword: z.string().optional(),
systemEmailUser: z.string().optional(),
systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(),
systemEmailIgnoreTLS: z.boolean().optional(),
systemEmailRequireTLS: z.boolean().optional(),
systemEmailRejectUnauthorized: z.boolean().optional(),
systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(),
});
+50
View File
@@ -0,0 +1,50 @@
import { z } from "zod";
import { passwordPattern, nameValidation, lowercaseEmailValidation } from "./shared.js";
//****************************************
// Auth Validations
//****************************************
export const loginValidation = z.object({
email: z.email("Must be a valid email address").transform((val) => val.toLowerCase()),
password: z.string().min(1, "Password is required"),
});
export const registrationBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
email: lowercaseEmailValidation,
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character"),
profileImage: z.any().optional(),
inviteToken: z.string().optional().default(""),
});
export const recoveryValidation = z.object({
email: z.email("Must be a valid email address"),
});
export const recoveryTokenBodyValidation = z.object({
recoveryToken: z.string().min(1, "Recovery token is required"),
});
export const newPasswordValidation = z.object({
recoveryToken: z.string().min(1, "Recovery token is required"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character"),
confirm: z.string().optional(),
});
export const inviteBodyValidation = z.object({
email: z.email("Must be a valid email address"),
role: z.array(z.string()).min(1, "At least one role is required"),
teamId: z.string().min(1, "Team ID is required"),
});
export const inviteVerificationBodyValidation = z.object({
token: z.string().min(1, "Token is required"),
});
+56
View File
@@ -0,0 +1,56 @@
import joi from "joi";
import { GeoContinents } from "@/types/geoCheck.js";
//****************************************
// Check Validations
//****************************************
export const ackCheckBodyValidation = joi.object({
ack: joi.boolean(),
});
export const ackAllChecksParamValidation = joi.object({
monitorId: joi.string().optional(),
path: joi.string().valid("monitor", "team").required(),
});
export const ackAllChecksBodyValidation = joi.object({
ack: joi.boolean(),
});
export const getChecksParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const getChecksQueryValidation = joi.object({
type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "up", "down", "resolve"),
ack: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
status: joi.boolean(),
continent: joi.alternatives().try(joi.string().valid(...GeoContinents), joi.array().items(joi.string().valid(...GeoContinents))),
});
export const getTeamChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "up", "down", "resolve"),
ack: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
});
export const deleteChecksParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const deleteChecksByTeamIdParamValidation = joi.object({});
export const updateChecksTTLBodyValidation = joi.object({
ttl: joi.number().required(),
});
+20
View File
@@ -0,0 +1,20 @@
/**
* Central validation exports
*
* This file re-exports all validation schemas from their respective modules.
* Import from here for convenience: import { loginValidation } from "@/validation";
*/
// Shared utilities
export * from "./shared.js";
// Domain-specific validations
export * from "./authValidation.js";
export * from "./monitorValidation.js";
export * from "./checkValidation.js";
export * from "./maintenanceWindowValidation.js";
export * from "./settingsValidation.js";
export * from "./statusPageValidation.js";
export * from "./notificationValidation.js";
export * from "./announcementValidation.js";
export * from "./userValidation.js";
-822
View File
@@ -1,822 +0,0 @@
import joi, { type CustomHelpers } from "joi";
import { type UserRole, UserRoles } from "@/types/user.js";
import { GeoContinents } from "@/types/geoCheck.js";
//****************************************
// Custom Validators
//****************************************
const roleValidatior = (role: UserRole[]) => (value: UserRole[], helpers: CustomHelpers) => {
const hasRole = role.some((role: UserRole) => value.includes(role));
if (!hasRole) {
return helpers.error("any.invalid", { message: `You do not have the required authorization. Required roles: ${role.join(", ")}` });
}
return value;
};
//****************************************
// Auth
//****************************************
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/]+$/;
const loginValidation = joi.object({
email: joi.string().email().required().lowercase(),
password: joi.string().required(),
});
const nameValidation = joi
.string()
.trim()
.max(50)
.pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s'\-().]+$/u)
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
"string.pattern.base":
"Names must contain at least 1 letter and may only include letters, currency symbols, spaces, apostrophes, hyphens (-), periods (.), and parentheses ().",
});
const registrationBodyValidation = joi.object({
firstName: nameValidation.required(),
lastName: nameValidation.required(),
email: joi
.string()
.email()
.required()
.custom((value, helpers) => {
const lowercasedValue = value.toLowerCase();
if (value !== lowercasedValue) {
return helpers.message({ custom: "Email must be in lowercase" });
}
return lowercasedValue;
}),
password: joi.string().min(8).required().pattern(passwordPattern),
profileImage: joi.any(),
inviteToken: joi.string().allow("").optional(),
});
const editUserBodyValidation = joi.object({
firstName: nameValidation.optional(),
lastName: nameValidation.optional(),
profileImage: joi.any(),
newPassword: joi.string().min(8).pattern(passwordPattern),
password: joi.string().min(8).pattern(passwordPattern),
deleteProfileImage: joi.alternatives().try(joi.boolean(), joi.string().valid("true", "false")),
});
const recoveryValidation = joi.object({
email: joi
.string()
.email({ tlds: { allow: false } })
.required(),
});
const recoveryTokenBodyValidation = joi.object({
recoveryToken: joi.string().required(),
});
const newPasswordValidation = joi.object({
recoveryToken: joi.string().required(),
password: joi.string().min(8).required().pattern(passwordPattern),
confirm: joi.string(),
});
const deleteUserParamValidation = joi.object({
email: joi.string().email().required(),
});
const inviteBodyValidation = joi.object({
email: joi.string().trim().email().required().messages({
"string.empty": "Email is required",
"string.email": "Must be a valid email address",
}),
role: joi.array().required(),
teamId: joi.string().required(),
});
const inviteVerificationBodyValidation = joi.object({
token: joi.string().required(),
});
//****************************************
// Monitors
//****************************************
const getMonitorByIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getMonitorByIdQueryValidation = joi.object({
status: joi.boolean(),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
numToDisplay: joi.number(),
normalize: joi.boolean(),
continent: joi.string().valid(...GeoContinents),
});
const getMonitorsByTeamIdParamValidation = joi.object({});
const getMonitorsByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"))
),
filter: joi.string().allow("", null),
});
const getMonitorsWithChecksQueryValidation = joi.object({
limit: joi.number().integer().min(1).max(100).optional(),
page: joi.number().integer().min(0).optional(),
rowsPerPage: joi.number().integer().min(1).max(100).optional(),
filter: joi.string().allow("", null).optional(),
field: joi.string().optional(),
order: joi.string().valid("asc", "desc").optional(),
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"))
)
.optional(),
explain: joi.boolean().optional(),
});
const getCertificateParamValidation = joi.object({
monitorId: joi.string().required(),
});
const createMonitorBodyValidation = joi.object({
_id: joi.string(),
name: joi.string().required(),
description: joi.string().allow(null, ""),
type: joi.string().required(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false),
useAdvancedMatching: joi.boolean().default(false),
port: joi.number(),
isActive: joi.boolean(),
interval: joi.number(),
cpuAlertThreshold: joi.number(),
memoryAlertThreshold: joi.number(),
diskAlertThreshold: joi.number(),
tempAlertThreshold: joi.number(),
notifications: joi.array().items(joi.string()),
secret: joi.string(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string().allow(null, ""),
gameId: joi.string().allow(""),
grpcServiceName: joi.string().allow("").default(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
geoCheckEnabled: joi.boolean().optional(),
geoCheckLocations: joi
.array()
.items(joi.string().valid(...GeoContinents))
.optional(),
geoCheckInterval: joi.number().min(300000).optional(),
});
const createMonitorsBodyValidation = joi.array().items(
createMonitorBodyValidation.keys({
userId: joi.string().required(),
teamId: joi.string().required(),
})
);
const editMonitorBodyValidation = joi
.object({
name: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
description: joi.string().allow(null, ""),
interval: joi.number(),
notifications: joi.array().items(joi.string()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
useAdvancedMatching: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string().allow(null, ""),
port: joi.number().min(1).max(65535),
cpuAlertThreshold: joi.number(),
memoryAlertThreshold: joi.number(),
diskAlertThreshold: joi.number(),
tempAlertThreshold: joi.number(),
gameId: joi.string().allow(""),
grpcServiceName: joi.string().allow(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
geoCheckEnabled: joi.boolean().optional(),
geoCheckLocations: joi
.array()
.items(joi.string().valid(...GeoContinents))
.optional(),
geoCheckInterval: joi.number().min(300000).optional(),
})
.options({ stripUnknown: true });
const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getMonitorURLByQueryValidation = joi.object({
monitorURL: joi.string().uri().required(),
});
const getHardwareDetailsByIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getHardwareDetailsByIdQueryValidation = joi.object({
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
});
//****************************************
// Alerts
//****************************************
const createAlertParamValidation = joi.object({
monitorId: joi.string().required(),
});
const createAlertBodyValidation = joi.object({
checkId: joi.string().required(),
monitorId: joi.string().required(),
userId: joi.string().required(),
status: joi.boolean(),
message: joi.string(),
notifiedStatus: joi.boolean(),
acknowledgeStatus: joi.boolean(),
});
const getAlertsByUserIdParamValidation = joi.object({
userId: joi.string().required(),
});
const getAlertsByMonitorIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getAlertByIdParamValidation = joi.object({
alertId: joi.string().required(),
});
const editAlertParamValidation = joi.object({
alertId: joi.string().required(),
});
const editAlertBodyValidation = joi.object({
status: joi.boolean(),
message: joi.string(),
notifiedStatus: joi.boolean(),
acknowledgeStatus: joi.boolean(),
});
const deleteAlertParamValidation = joi.object({
alertId: joi.string().required(),
});
//****************************************
// Checks
//****************************************
const createCheckParamValidation = joi.object({
monitorId: joi.string().required(),
});
const createCheckBodyValidation = joi.object({
monitorId: joi.string().required(),
status: joi.boolean().required(),
responseTime: joi.number().required(),
statusCode: joi.number().required(),
message: joi.string().required(),
});
const ackCheckBodyValidation = joi.object({
ack: joi.boolean(),
});
const ackAllChecksParamValidation = joi.object({
monitorId: joi.string().optional(),
path: joi.string().valid("monitor", "team").required(),
});
const ackAllChecksBodyValidation = joi.object({
ack: joi.boolean(),
});
const getChecksParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getChecksQueryValidation = joi.object({
type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "up", "down", "resolve"),
ack: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
status: joi.boolean(),
continent: joi.alternatives().try(joi.string().valid(...GeoContinents), joi.array().items(joi.string().valid(...GeoContinents))),
});
const getTeamChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "up", "down", "resolve"),
ack: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
});
const deleteChecksParamValidation = joi.object({
monitorId: joi.string().required(),
});
const deleteChecksByTeamIdParamValidation = joi.object({});
const updateChecksTTLBodyValidation = joi.object({
ttl: joi.number().required(),
});
//****************************************
// PageSpeedCheckValidation
//****************************************
const getPageSpeedCheckParamValidation = joi.object({
monitorId: joi.string().required(),
});
//Validation schema for the monitorId parameter
const createPageSpeedCheckParamValidation = joi.object({
monitorId: joi.string().required(),
});
//Validation schema for the monitorId body
const createPageSpeedCheckBodyValidation = joi.object({
url: joi.string().required(),
});
const deletePageSpeedCheckParamValidation = joi.object({
monitorId: joi.string().required(),
});
//****************************************
// MaintenanceWindowValidation
//****************************************
const createMaintenanceWindowBodyValidation = joi.object({
monitors: joi.array().items(joi.string()).required(),
name: joi.string().required(),
active: joi.boolean(),
duration: joi.number().required(),
durationUnit: joi.string().valid("seconds", "minutes", "hours", "days").required(),
start: joi.date().required(),
end: joi.date().required(),
repeat: joi.number().required(),
expiry: joi.date(),
});
const getMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
const getMaintenanceWindowsByTeamIdQueryValidation = joi.object({
active: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
field: joi.string(),
order: joi.string().valid("asc", "desc"),
});
const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
const deleteMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
const editMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
const editMaintenanceByIdWindowBodyValidation = joi.object({
active: joi.boolean(),
name: joi.string(),
repeat: joi.number(),
start: joi.date(),
end: joi.date(),
expiry: joi.date(),
monitors: joi.array(),
duration: joi.number(),
durationUnit: joi.string().valid("seconds", "minutes", "hours", "days"),
});
//****************************************
// SettingsValidation
//****************************************
const updateAppSettingsBodyValidation = joi.object({
checkTTL: joi.number().allow(""),
pagespeedApiKey: joi.string().allow(""),
language: joi.string().allow(""),
timezone: joi.string().allow(""),
showURL: joi.bool().optional(),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
systemEmailConnectionHost: joi.string().allow(""),
systemEmailTLSServername: joi.string().allow(""),
systemEmailSecure: joi.boolean(),
systemEmailPool: joi.boolean(),
systemEmailIgnoreTLS: joi.boolean(),
systemEmailRequireTLS: joi.boolean(),
systemEmailRejectUnauthorized: joi.boolean(),
globalThresholds: joi
.object({
cpu: joi.number().min(1).max(100).allow(""),
memory: joi.number().min(1).max(100).allow(""),
disk: joi.number().min(1).max(100).allow(""),
temperature: joi.number().min(1).max(150).allow(""),
})
.optional(),
});
//****************************************
// Status Page Validation
//****************************************
const getStatusPageParamValidation = joi.object({
url: joi.string().required(),
});
const getStatusPageQueryValidation = joi.object({
type: joi.string().valid("uptime").required(),
timeFrame: joi.number().optional(),
});
const createStatusPageBodyValidation = joi.object({
type: joi.string().valid("uptime").required(),
companyName: joi.string().required(),
url: joi
.string()
.pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen
.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(),
});
const imageValidation = joi
.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)",
}),
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(),
})
.messages({
"any.required": "Image file is required",
});
const webhookConfigValidation = joi
.object({
webhookUrl: joi
.string()
.uri()
.when("$platform", {
switch: [
{
is: "telegram",
then: joi.optional(),
},
{
is: "discord",
then: joi.required().messages({
"string.empty": "Discord webhook URL is required",
"string.uri": "Discord webhook URL must be a valid URL",
"any.required": "Discord webhook URL is required",
}),
},
{
is: "slack",
then: joi.required().messages({
"string.empty": "Slack webhook URL is required",
"string.uri": "Slack webhook URL must be a valid URL",
"any.required": "Slack webhook URL is required",
}),
},
],
}),
botToken: joi.string().when("$platform", {
is: "telegram",
then: joi.required().messages({
"string.empty": "Telegram bot token is required",
"any.required": "Telegram bot token is required",
}),
otherwise: joi.optional(),
}),
chatId: joi.string().when("$platform", {
is: "telegram",
then: joi.required().messages({
"string.empty": "Telegram chat ID is required",
"any.required": "Telegram chat ID is required",
}),
otherwise: joi.optional(),
}),
})
.required();
const triggerNotificationBodyValidation = joi.object({
monitorId: joi.string().required().messages({
"string.empty": "Monitor ID is required",
"any.required": "Monitor ID is required",
}),
type: joi.string().valid("webhook").required().messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be webhook",
}),
platform: joi.string().valid("telegram", "discord", "slack").required().messages({
"string.empty": "Platform type is required",
"any.required": "Platform type is required",
"any.only": "Platform must be telegram, discord, or slack",
}),
config: webhookConfigValidation.required().messages({
"any.required": "Webhook configuration is required",
}),
});
const createNotificationBodyValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty", "matrix").required().messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, slack, discord, pager_duty, or matrix",
}),
address: joi.when("type", {
switch: [
{
is: "email",
then: joi.string().email().required().messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
},
{
is: "pager_duty",
then: joi.string().required().messages({
"string.empty": "PagerDuty integration key cannot be empty",
"any.required": "PagerDuty integration key is required",
}),
},
{
is: joi.string().valid("webhook", "slack", "discord"),
then: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
},
{
is: "matrix",
then: joi.string().allow("").optional(),
},
],
}),
homeserverUrl: joi.when("type", {
is: "matrix",
then: joi.string().uri().required().messages({
"string.empty": "Homeserver URL cannot be empty",
"any.required": "Homeserver URL is required",
"string.uri": "Please enter a valid Homeserver URL",
}),
otherwise: joi.string().allow("").optional(),
}),
roomId: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Room ID cannot be empty",
"any.required": "Room ID is required",
}),
otherwise: joi.string().allow("").optional(),
}),
accessToken: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Access Token cannot be empty",
"any.required": "Access Token is required",
}),
otherwise: joi.string().allow("").optional(),
}),
});
//****************************************
// Announcetment Page Validation
//****************************************
const createAnnouncementValidation = joi.object({
title: joi.string().required().messages({
"string.empty": "Title cannot be empty",
"any.required": "Title is required",
}),
message: joi.string().required().messages({
"string.empty": "Message cannot be empty",
"any.required": "Message is required",
}),
userId: joi.string().required(),
});
const sendTestEmailBodyValidation = joi.object({
to: joi.string().required(),
systemEmailHost: joi.string(),
systemEmailPort: joi.number(),
systemEmailSecure: joi.boolean(),
systemEmailPool: joi.boolean(),
systemEmailAddress: joi.string(),
systemEmailPassword: joi.string(),
systemEmailUser: joi.string(),
systemEmailConnectionHost: joi.string().allow("").optional(),
systemEmailIgnoreTLS: joi.boolean(),
systemEmailRequireTLS: joi.boolean(),
systemEmailRejectUnauthorized: joi.boolean(),
systemEmailTLSServername: joi.string().allow("").optional(),
});
const getUserByIdParamValidation = joi.object({
userId: joi.string().required(),
});
const editUserByIdParamValidation = joi.object({
userId: joi.string().required(),
});
const editUserByIdBodyValidation = joi.object({
firstName: nameValidation.required(),
lastName: nameValidation.required(),
role: joi
.array()
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});
const editSuperadminUserByIdBodyValidation = joi.object({
firstName: nameValidation.required(),
lastName: nameValidation.required(),
role: joi
.array()
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});
const editUserPasswordByIdBodyValidation = joi.object({
password: joi.string().min(8).required().pattern(passwordPattern),
});
const createUserBodyValidation = joi.object({
firstName: nameValidation.required(),
lastName: nameValidation.required(),
email: joi
.string()
.email()
.required()
.custom((value, helpers) => {
const lowercasedValue = value.toLowerCase();
if (value !== lowercasedValue) {
return helpers.message({ custom: "Email must be in lowercase" });
}
return lowercasedValue;
}),
password: joi.string().min(8).required().pattern(passwordPattern),
role: joi
.array()
.items(joi.string().valid(...UserRoles))
.min(1)
.required(),
});
export {
roleValidatior,
loginValidation,
registrationBodyValidation,
recoveryValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
inviteBodyValidation,
inviteVerificationBodyValidation,
createMonitorBodyValidation,
createMonitorsBodyValidation,
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
getCertificateParamValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
getMonitorURLByQueryValidation,
editUserBodyValidation,
createAlertParamValidation,
createAlertBodyValidation,
getAlertsByUserIdParamValidation,
getAlertsByMonitorIdParamValidation,
getAlertByIdParamValidation,
editAlertParamValidation,
editAlertBodyValidation,
deleteAlertParamValidation,
createCheckParamValidation,
createCheckBodyValidation,
getChecksParamValidation,
getChecksQueryValidation,
getTeamChecksQueryValidation,
ackCheckBodyValidation,
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
deleteChecksParamValidation,
deleteChecksByTeamIdParamValidation,
updateChecksTTLBodyValidation,
deleteUserParamValidation,
getPageSpeedCheckParamValidation,
createPageSpeedCheckParamValidation,
deletePageSpeedCheckParamValidation,
createPageSpeedCheckBodyValidation,
createMaintenanceWindowBodyValidation,
getMaintenanceWindowByIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
getMaintenanceWindowsByMonitorIdParamValidation,
deleteMaintenanceWindowByIdParamValidation,
editMaintenanceWindowByIdParamValidation,
editMaintenanceByIdWindowBodyValidation,
updateAppSettingsBodyValidation,
createStatusPageBodyValidation,
getStatusPageParamValidation,
getStatusPageQueryValidation,
imageValidation,
triggerNotificationBodyValidation,
createNotificationBodyValidation,
webhookConfigValidation,
createAnnouncementValidation,
sendTestEmailBodyValidation,
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
editUserPasswordByIdBodyValidation,
createUserBodyValidation,
};
@@ -0,0 +1,53 @@
import joi from "joi";
//****************************************
// Maintenance Window Validations
//****************************************
export const createMaintenanceWindowBodyValidation = joi.object({
monitors: joi.array().items(joi.string()).required(),
name: joi.string().required(),
active: joi.boolean(),
duration: joi.number().required(),
durationUnit: joi.string().valid("seconds", "minutes", "hours", "days").required(),
start: joi.date().required(),
end: joi.date().required(),
repeat: joi.number().required(),
expiry: joi.date(),
});
export const getMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
export const getMaintenanceWindowsByTeamIdQueryValidation = joi.object({
active: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
field: joi.string(),
order: joi.string().valid("asc", "desc"),
});
export const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const deleteMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
export const editMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
export const editMaintenanceByIdWindowBodyValidation = joi.object({
active: joi.boolean(),
name: joi.string(),
repeat: joi.number(),
start: joi.date(),
end: joi.date(),
expiry: joi.date(),
monitors: joi.array(),
duration: joi.number(),
durationUnit: joi.string().valid("seconds", "minutes", "hours", "days"),
});
+127
View File
@@ -0,0 +1,127 @@
import joi from "joi";
import { GeoContinents } from "@/types/geoCheck.js";
export const getMonitorByIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const getMonitorByIdQueryValidation = joi.object({
status: joi.boolean(),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
numToDisplay: joi.number(),
normalize: joi.boolean(),
continent: joi.string().valid(...GeoContinents),
});
export const getMonitorsByTeamIdParamValidation = joi.object({});
export const getMonitorsByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"))
),
filter: joi.string().allow("", null),
});
export const getMonitorsWithChecksQueryValidation = joi.object({
limit: joi.number().integer().min(1).max(100).optional(),
page: joi.number().integer().min(0).optional(),
rowsPerPage: joi.number().integer().min(1).max(100).optional(),
filter: joi.string().allow("", null).optional(),
field: joi.string().optional(),
order: joi.string().valid("asc", "desc").optional(),
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"))
)
.optional(),
explain: joi.boolean().optional(),
});
export const getCertificateParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const createMonitorBodyValidation = joi.object({
_id: joi.string(),
name: joi.string().required(),
description: joi.string().allow(null, ""),
type: joi.string().required(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false),
useAdvancedMatching: joi.boolean().default(false),
port: joi.number(),
isActive: joi.boolean(),
interval: joi.number(),
cpuAlertThreshold: joi.number(),
memoryAlertThreshold: joi.number(),
diskAlertThreshold: joi.number(),
tempAlertThreshold: joi.number(),
notifications: joi.array().items(joi.string()),
secret: joi.string(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string().allow(null, ""),
gameId: joi.string().allow(""),
grpcServiceName: joi.string().allow("").default(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
geoCheckEnabled: joi.boolean().optional(),
geoCheckLocations: joi
.array()
.items(joi.string().valid(...GeoContinents))
.optional(),
geoCheckInterval: joi.number().min(300000).optional(),
});
export const editMonitorBodyValidation = joi
.object({
name: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
description: joi.string().allow(null, ""),
interval: joi.number(),
notifications: joi.array().items(joi.string()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
useAdvancedMatching: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string().allow(null, ""),
port: joi.number().min(1).max(65535),
cpuAlertThreshold: joi.number(),
memoryAlertThreshold: joi.number(),
diskAlertThreshold: joi.number(),
tempAlertThreshold: joi.number(),
gameId: joi.string().allow(""),
grpcServiceName: joi.string().allow(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
geoCheckEnabled: joi.boolean().optional(),
geoCheckLocations: joi
.array()
.items(joi.string().valid(...GeoContinents))
.optional(),
geoCheckInterval: joi.number().min(300000).optional(),
})
.options({ stripUnknown: true });
export const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const getHardwareDetailsByIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
export const getHardwareDetailsByIdQueryValidation = joi.object({
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
});
@@ -0,0 +1,78 @@
import joi from "joi";
//****************************************
// Notification Validations
//****************************************
export const createNotificationBodyValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty", "matrix").required().messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, slack, discord, pager_duty, or matrix",
}),
address: joi.when("type", {
switch: [
{
is: "email",
then: joi.string().email().required().messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
},
{
is: "pager_duty",
then: joi.string().required().messages({
"string.empty": "PagerDuty integration key cannot be empty",
"any.required": "PagerDuty integration key is required",
}),
},
{
is: joi.string().valid("webhook", "slack", "discord"),
then: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
},
{
is: "matrix",
then: joi.string().allow("").optional(),
},
],
}),
homeserverUrl: joi.when("type", {
is: "matrix",
then: joi.string().uri().required().messages({
"string.empty": "Homeserver URL cannot be empty",
"any.required": "Homeserver URL is required",
"string.uri": "Please enter a valid Homeserver URL",
}),
otherwise: joi.string().allow("").optional(),
}),
roomId: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Room ID cannot be empty",
"any.required": "Room ID is required",
}),
otherwise: joi.string().allow("").optional(),
}),
accessToken: joi.when("type", {
is: "matrix",
then: joi.string().required().messages({
"string.empty": "Access Token cannot be empty",
"any.required": "Access Token is required",
}),
otherwise: joi.string().allow("").optional(),
}),
});
@@ -0,0 +1,34 @@
import joi from "joi";
//****************************************
// Settings Validations
//****************************************
export const updateAppSettingsBodyValidation = joi.object({
checkTTL: joi.number().allow(""),
pagespeedApiKey: joi.string().allow(""),
language: joi.string().allow(""),
timezone: joi.string().allow(""),
showURL: joi.bool().optional(),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
systemEmailConnectionHost: joi.string().allow(""),
systemEmailTLSServername: joi.string().allow(""),
systemEmailSecure: joi.boolean(),
systemEmailPool: joi.boolean(),
systemEmailIgnoreTLS: joi.boolean(),
systemEmailRequireTLS: joi.boolean(),
systemEmailRejectUnauthorized: joi.boolean(),
globalThresholds: joi
.object({
cpu: joi.number().min(1).max(100).allow(""),
memory: joi.number().min(1).max(100).allow(""),
disk: joi.number().min(1).max(100).allow(""),
temperature: joi.number().min(1).max(150).allow(""),
})
.optional(),
});
+39
View File
@@ -0,0 +1,39 @@
import { z } from "zod";
import { type UserRole } from "@/types/user.js";
/**
* Password pattern: requires at least one lowercase, uppercase, number, and special character
*/
export const passwordPattern =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/]+$/;
/**
* Reusable name validation schema
*/
export const nameValidation = z
.string()
.trim()
.max(50, "Name must be less than 50 characters")
.regex(
/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s'\-().]+$/u,
"Names must contain at least 1 letter and may only include letters, currency symbols, spaces, apostrophes, hyphens (-), periods (.), and parentheses ()."
);
/**
* Reusable email validation with lowercase enforcement
*/
export const lowercaseEmailValidation = z
.email()
.transform((val) => val.toLowerCase())
.refine((val) => val === val.toLowerCase(), {
message: "Email must be in lowercase",
});
/**
* Custom validator for role-based authorization
*/
export const roleValidator = (allowedRoles: UserRole[]) => {
return z.array(z.custom<UserRole>()).refine((userRoles) => allowedRoles.some((role) => userRoles.includes(role)), {
message: `You do not have the required authorization. Required roles: ${allowedRoles.join(", ")}`,
});
};
@@ -0,0 +1,68 @@
import joi from "joi";
//****************************************
// Status Page Validations
//****************************************
export const getStatusPageParamValidation = joi.object({
url: joi.string().required(),
});
export const getStatusPageQueryValidation = joi.object({
type: joi.string().valid("uptime").required(),
timeFrame: joi.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
.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)",
}),
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(),
})
.messages({
"any.required": "Image file is required",
});
+52
View File
@@ -0,0 +1,52 @@
import { z } from "zod";
import { UserRoles } from "@/types/user.js";
import { nameValidation, lowercaseEmailValidation, passwordPattern } from "./shared.js";
//****************************************
// User Validations
//****************************************
export const getUserByIdParamValidation = z.object({
userId: z.string().min(1, "User ID is required"),
});
export const editUserByIdParamValidation = z.object({
userId: z.string().min(1, "User ID is required"),
});
export const editUserByIdBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
});
export const editSuperadminUserByIdBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
});
export const editUserPasswordByIdBodyValidation = z.object({
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character"),
});
export const createUserBodyValidation = z.object({
firstName: nameValidation,
lastName: nameValidation,
email: lowercaseEmailValidation,
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(passwordPattern, "Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character"),
role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"),
});
export const editUserBodyValidation = z.object({
firstName: nameValidation.optional(),
lastName: nameValidation.optional(),
email: lowercaseEmailValidation.optional(),
profilePicture: z.string().optional(),
});