diff --git a/server/package-lock.json b/server/package-lock.json index 30b610df5..131a3708e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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" + } } } } diff --git a/server/package.json b/server/package.json index 9e005bca4..3abadb75f 100755 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 3fa551808..8add39087 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -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" }); diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index 0c085f6d3..39c4db02a 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -10,7 +10,7 @@ import { ackCheckBodyValidation, ackAllChecksParamValidation, ackAllChecksBodyValidation, -} from "@/validation/joi.js"; +} from "@/validation/checkValidation.js"; const SERVICE_NAME = "checkController"; diff --git a/server/src/controllers/geoCheckController.ts b/server/src/controllers/geoCheckController.ts index ec9d489f1..056599000 100644 --- a/server/src/controllers/geoCheckController.ts +++ b/server/src/controllers/geoCheckController.ts @@ -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"; diff --git a/server/src/controllers/inviteController.ts b/server/src/controllers/inviteController.ts index bba04fbbd..b17d32e98 100755 --- a/server/src/controllers/inviteController.ts +++ b/server/src/controllers/inviteController.ts @@ -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, diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index 187e479cc..d4c99a549 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -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"; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index a11e3fc65..4dba18abc 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -12,7 +12,7 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, -} from "@/validation/joi.js"; +} from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; import { fetchMonitorCertificate, diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index f32f80ddc..77e332cad 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -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"; diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index 3cbfd3f6d..354f34858 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -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, diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index b936ff5ad..ccffff7a9 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -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"; diff --git a/server/src/validation/announcementValidation.ts b/server/src/validation/announcementValidation.ts new file mode 100644 index 000000000..5f3514709 --- /dev/null +++ b/server/src/validation/announcementValidation.ts @@ -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(), +}); diff --git a/server/src/validation/authValidation.ts b/server/src/validation/authValidation.ts new file mode 100644 index 000000000..045868c40 --- /dev/null +++ b/server/src/validation/authValidation.ts @@ -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"), +}); diff --git a/server/src/validation/checkValidation.ts b/server/src/validation/checkValidation.ts new file mode 100644 index 000000000..b88415876 --- /dev/null +++ b/server/src/validation/checkValidation.ts @@ -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(), +}); diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts new file mode 100644 index 000000000..cd04ae2d2 --- /dev/null +++ b/server/src/validation/index.ts @@ -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"; diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts deleted file mode 100755 index 415f3732c..000000000 --- a/server/src/validation/joi.ts +++ /dev/null @@ -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, -}; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts new file mode 100644 index 000000000..ac3e32966 --- /dev/null +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -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"), +}); diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts new file mode 100644 index 000000000..37c669ca3 --- /dev/null +++ b/server/src/validation/monitorValidation.ts @@ -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"), +}); diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts new file mode 100644 index 000000000..73e6750f8 --- /dev/null +++ b/server/src/validation/notificationValidation.ts @@ -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(), + }), +}); diff --git a/server/src/validation/settingsValidation.ts b/server/src/validation/settingsValidation.ts new file mode 100644 index 000000000..6321f13f0 --- /dev/null +++ b/server/src/validation/settingsValidation.ts @@ -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(), +}); diff --git a/server/src/validation/shared.ts b/server/src/validation/shared.ts new file mode 100644 index 000000000..ce01705ea --- /dev/null +++ b/server/src/validation/shared.ts @@ -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()).refine((userRoles) => allowedRoles.some((role) => userRoles.includes(role)), { + message: `You do not have the required authorization. Required roles: ${allowedRoles.join(", ")}`, + }); +}; diff --git a/server/src/validation/statusPageValidation.ts b/server/src/validation/statusPageValidation.ts new file mode 100644 index 000000000..bca638fc4 --- /dev/null +++ b/server/src/validation/statusPageValidation.ts @@ -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", + }); diff --git a/server/src/validation/userValidation.ts b/server/src/validation/userValidation.ts new file mode 100644 index 000000000..9e11c8c57 --- /dev/null +++ b/server/src/validation/userValidation.ts @@ -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(), +});