diff --git a/server/package-lock.json b/server/package-lock.json index 30b610df5..42c2388dc 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -30,7 +30,6 @@ "ioredis": "^5.4.2", "isomorphic-dompurify": "^2.26.0", "jmespath": "^0.16.0", - "joi": "^17.13.1", "jsdom": "^26.1.0", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", @@ -43,7 +42,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 +876,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 +1506,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1549,6 +1551,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2227,21 +2230,6 @@ "node": ">=6" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3456,27 +3444,6 @@ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "license": "MIT" }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, "node_modules/@sinclair/typebox": { "version": "0.34.47", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", @@ -4376,6 +4343,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 +4504,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 +4991,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5600,6 +5570,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6542,6 +6513,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 +7286,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 +7648,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 +9297,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9919,19 +9894,6 @@ "node": ">= 0.6.0" } }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12212,6 +12174,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14436,6 +14399,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 +14562,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15402,6 +15367,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..ee30cccee 100755 --- a/server/package.json +++ b/server/package.json @@ -45,7 +45,6 @@ "ioredis": "^5.4.2", "isomorphic-dompurify": "^2.26.0", "jmespath": "^0.16.0", - "joi": "^17.13.1", "jsdom": "^26.1.0", "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", @@ -58,7 +57,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..ec6722bd3 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -7,10 +7,7 @@ import { deleteChecksParamValidation, deleteChecksByTeamIdParamValidation, updateChecksTTLBodyValidation, - ackCheckBodyValidation, - ackAllChecksParamValidation, - ackAllChecksBodyValidation, -} from "@/validation/joi.js"; +} from "@/validation/checkValidation.js"; const SERVICE_NAME = "checkController"; @@ -28,8 +25,8 @@ class CheckController { getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - await getChecksParamValidation.validateAsync(req.params); - await getChecksQueryValidation.validateAsync(req.query); + getChecksParamValidation.parse(req.params); + getChecksQueryValidation.parse(req.query); const result = await this.checkService.getChecksByMonitor({ monitorId: req?.params?.monitorId, @@ -49,7 +46,7 @@ class CheckController { getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { try { - await getTeamChecksQueryValidation.validateAsync(req.query); + getTeamChecksQueryValidation.parse(req.query); const checkData = await this.checkService.getChecksByTeam({ teamId: req?.user?.teamId, query: req?.query, @@ -80,7 +77,7 @@ class CheckController { deleteChecks = async (req: Request, res: Response, next: NextFunction) => { try { - await deleteChecksParamValidation.validateAsync(req.params); + deleteChecksParamValidation.parse(req.params); const deletedCount = await this.checkService.deleteChecks({ monitorId: req.params.monitorId as string, @@ -99,7 +96,7 @@ class CheckController { deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - await deleteChecksByTeamIdParamValidation.validateAsync(req.params); + deleteChecksByTeamIdParamValidation.parse(req.params); const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); @@ -115,7 +112,7 @@ class CheckController { updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => { try { - await updateChecksTTLBodyValidation.validateAsync(req.body); + updateChecksTTLBodyValidation.parse(req.body); await this.checkService.updateChecksTTL({ teamId: req?.user?.teamId, diff --git a/server/src/controllers/geoCheckController.ts b/server/src/controllers/geoCheckController.ts index ec9d489f1..cfa7fe056 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"; @@ -18,8 +18,8 @@ class GeoCheckController { getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - await getChecksParamValidation.validateAsync(req.params); - await getChecksQueryValidation.validateAsync(req.query); + getChecksParamValidation.parse(req.params); + getChecksQueryValidation.parse(req.query); const result = await this.geoChecksService.getGeoChecksByMonitor({ monitorId: req?.params?.monitorId as string, 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..ed16911bf 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"; @@ -25,7 +25,7 @@ class MaintenanceWindowController { createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { try { - await createMaintenanceWindowBodyValidation.validateAsync(req.body); + createMaintenanceWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req?.user?.teamId); await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); @@ -40,7 +40,7 @@ class MaintenanceWindowController { }; getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { try { - await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); + getMaintenanceWindowByIdParamValidation.parse(req.params); const teamId = requireTeamId(req.user?.teamId); @@ -58,7 +58,7 @@ class MaintenanceWindowController { getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); + getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); @@ -76,7 +76,7 @@ class MaintenanceWindowController { getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); + getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); @@ -96,7 +96,7 @@ class MaintenanceWindowController { }; deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { try { - await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); + deleteMaintenanceWindowByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); @@ -113,8 +113,8 @@ class MaintenanceWindowController { editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { try { - await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); - await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); + editMaintenanceWindowByIdParamValidation.parse(req.params); + editMaintenanceByIdWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req.user?.teamId); diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index a11e3fc65..1d222e6e2 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, @@ -45,7 +45,7 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { - await getCertificateParamValidation.validateAsync(req.params); + getCertificateParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); const monitorId = requireString(req.params?.monitorId, "Monitor ID"); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -88,8 +88,8 @@ class MonitorController { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - await getHardwareDetailsByIdParamValidation.validateAsync(req.params); - await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + getHardwareDetailsByIdParamValidation.parse(req.params); + getHardwareDetailsByIdQueryValidation.parse(req.query); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent"; @@ -112,8 +112,8 @@ class MonitorController { }; getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - await getHardwareDetailsByIdParamValidation.validateAsync(req.params); - await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + getHardwareDetailsByIdParamValidation.parse(req.params); + getHardwareDetailsByIdQueryValidation.parse(req.query); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const dateRange = requireString(req?.query?.dateRange, "dateRange"); @@ -137,8 +137,8 @@ class MonitorController { getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - await getMonitorByIdParamValidation.validateAsync(req.params); - await getMonitorByIdQueryValidation.validateAsync(req.query); + getMonitorByIdParamValidation.parse(req.params); + getMonitorByIdQueryValidation.parse(req.query); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const dateRange = requireString(req?.query?.dateRange, "dateRange"); @@ -169,8 +169,8 @@ class MonitorController { getMonitorById = async (req: Request, res: Response, next: NextFunction) => { try { - await getMonitorByIdParamValidation.validateAsync(req.params); - await getMonitorByIdQueryValidation.validateAsync(req.query); + getMonitorByIdParamValidation.parse(req.params); + getMonitorByIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); @@ -189,7 +189,7 @@ class MonitorController { createMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - await createMonitorBodyValidation.validateAsync(req.body); + createMonitorBodyValidation.parse(req.body); const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); @@ -231,7 +231,7 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - await getMonitorByIdParamValidation.validateAsync(req.params); + getMonitorByIdParamValidation.parse(req.params); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const teamId = requireTeamId(req?.user?.teamId); @@ -264,8 +264,8 @@ class MonitorController { editMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - await getMonitorByIdParamValidation.validateAsync(req.params); - await editMonitorBodyValidation.validateAsync(req.body); + getMonitorByIdParamValidation.parse(req.params); + editMonitorBodyValidation.parse(req.body); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const teamId = requireTeamId(req?.user?.teamId); @@ -283,7 +283,7 @@ class MonitorController { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - await pauseMonitorParamValidation.validateAsync(req.params); + pauseMonitorParamValidation.parse(req.params); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); const teamId = requireTeamId(req?.user?.teamId); @@ -336,8 +336,8 @@ class MonitorController { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + getMonitorsByTeamIdParamValidation.parse(req.params); + getMonitorsByTeamIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); const type = parseMonitorTypeFilter(req.query?.type); @@ -357,8 +357,8 @@ class MonitorController { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsWithChecksQueryValidation.validateAsync(req.query); + getMonitorsByTeamIdParamValidation.parse(req.params); + getMonitorsWithChecksQueryValidation.parse(req.query); const explain = optionalBoolean(req?.query?.explain, "explain"); const limit = optionalNumber(req?.query?.limit, "limit"); const page = optionalNumber(req?.query?.page, "page"); diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index f32f80ddc..0cee3726e 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"; @@ -42,9 +42,7 @@ class NotificationController { createNotification = async (req: Request, res: Response, next: NextFunction) => { try { - await createNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - }); + createNotificationBodyValidation.parse(req.body); const body = req.body; @@ -134,9 +132,7 @@ class NotificationController { editNotification = async (req: Request, res: Response, next: NextFunction) => { try { - await createNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - }); + createNotificationBodyValidation.parse(req.body); const teamId = requireTeamId(req.user?.teamId); const notificationId = req.params.id as string; diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index 3cbfd3f6d..d3caa0d8f 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/notificationValidation.js"; import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "SettingsController"; @@ -55,20 +56,24 @@ class SettingsController { }; updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { - await updateAppSettingsBodyValidation.validateAsync(req.body); + try { + updateAppSettingsBodyValidation.parse(req.body); - const updatedSettings = await this.settingsService.updateDbSettings(req.body); - const returnSettings = this.buildAppSettings(updatedSettings); - return res.status(200).json({ - success: true, - msg: "App settings updated successfully", - data: returnSettings, - }); + const updatedSettings = await this.settingsService.updateDbSettings(req.body); + const returnSettings = this.buildAppSettings(updatedSettings); + return res.status(200).json({ + success: true, + msg: "App settings updated successfully", + data: returnSettings, + }); + } catch (error) { + next(error); + } }; 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..fa6cd3288 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"; @@ -28,8 +33,10 @@ class StatusPageController { createStatusPage = async (req: Request, res: Response, next: NextFunction) => { try { - await createStatusPageBodyValidation.validateAsync(req.body); - await imageValidation.validateAsync(req.file); + createStatusPageBodyValidation.parse(req.body); + if (req.file) { + imageValidation.parse(req.file); + } const teamId = requireTeamId(req?.user?.teamId); const userId = requireUserId(req?.user?.id); @@ -47,8 +54,10 @@ class StatusPageController { updateStatusPage = async (req: Request, res: Response, next: NextFunction) => { try { - await createStatusPageBodyValidation.validateAsync(req.body); - await imageValidation.validateAsync(req.file); + createStatusPageBodyValidation.parse(req.body); + if (req.file) { + imageValidation.parse(req.file); + } const teamId = requireTeamId(req?.user?.teamId); const statusPageId = req.params.id as string; if (!statusPageId) { @@ -70,8 +79,8 @@ class StatusPageController { getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => { try { - await getStatusPageParamValidation.validateAsync(req.params); - await getStatusPageQueryValidation.validateAsync(req.query); + getStatusPageParamValidation.parse(req.params); + getStatusPageQueryValidation.parse(req.query); if (!req.params.url) { throw new AppError({ message: "Status page URL is required", status: 400 }); diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 730072d8c..3c4225b50 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -3,7 +3,6 @@ import { NotificationModel, type NotificationDocument } from "@/db/models/index. import { INotificationsRepository } from "@/repositories/index.js"; import type { Notification } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; -import { not } from "joi"; class MongoNotificationsRepository implements INotificationsRepository { private mapDocuments = (documents: NotificationDocument[]): Notification[] => { 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..547281d68 --- /dev/null +++ b/server/src/validation/checkValidation.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; +import { booleanCoercion } from "./shared.js"; +import { GeoContinents } from "@/types/geoCheck.js"; + +//**************************************** +// Check Validations +//**************************************** + +export const getChecksParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getChecksQueryValidation = z.object({ + type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"]).optional(), + sortOrder: z.enum(["asc", "desc"]).optional(), + limit: z.coerce.number().optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + filter: z.enum(["all", "up", "down", "resolve"]).optional(), + ack: booleanCoercion.optional(), + page: z.coerce.number().optional(), + rowsPerPage: z.coerce.number().optional(), + status: booleanCoercion.optional(), + continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), +}); + +export const getTeamChecksQueryValidation = z.object({ + sortOrder: z.enum(["asc", "desc"]).optional(), + limit: z.coerce.number().optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + filter: z.enum(["all", "up", "down", "resolve"]).optional(), + ack: booleanCoercion.optional(), + page: z.coerce.number().optional(), + rowsPerPage: z.coerce.number().optional(), +}); + +export const deleteChecksParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const deleteChecksByTeamIdParamValidation = z.object({}); + +export const updateChecksTTLBodyValidation = z.object({ + ttl: z.number().min(1, "TTL is required"), +}); diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts new file mode 100644 index 000000000..4d7140d01 --- /dev/null +++ b/server/src/validation/index.ts @@ -0,0 +1,19 @@ +/** + * 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 "./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..e09ac2989 --- /dev/null +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import { booleanCoercion } from "./shared.js"; + +//**************************************** +// Maintenance Window Validations +//**************************************** + +export const createMaintenanceWindowBodyValidation = z.object({ + monitors: z.array(z.string()).min(1, "At least one monitor is required"), + name: z.string().min(1, "Name is required"), + active: z.boolean().optional(), + duration: z.number().min(1, "Duration is required"), + durationUnit: z.enum(["seconds", "minutes", "hours", "days"]), + start: z.coerce.date(), + end: z.coerce.date(), + repeat: z.number().min(0, "Repeat must be a non-negative number"), + expiry: z.coerce.date().optional(), +}); + +export const getMaintenanceWindowByIdParamValidation = z.object({ + id: z.string().min(1, "ID is required"), +}); + +export const getMaintenanceWindowsByTeamIdQueryValidation = z.object({ + active: booleanCoercion.optional(), + page: z.coerce.number().optional(), + rowsPerPage: z.coerce.number().optional(), + field: z.string().optional(), + order: z.enum(["asc", "desc"]).optional(), +}); + +export const getMaintenanceWindowsByMonitorIdParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const deleteMaintenanceWindowByIdParamValidation = z.object({ + id: z.string().min(1, "ID is required"), +}); + +export const editMaintenanceWindowByIdParamValidation = z.object({ + id: z.string().min(1, "ID is required"), +}); + +export const editMaintenanceByIdWindowBodyValidation = z.object({ + active: z.boolean().optional(), + name: z.string().optional(), + repeat: z.number().optional(), + start: z.coerce.date().optional(), + end: z.coerce.date().optional(), + expiry: z.coerce.date().optional(), + monitors: z.array(z.unknown()).optional(), + duration: z.number().optional(), + durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), +}); diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts new file mode 100644 index 000000000..4694143ac --- /dev/null +++ b/server/src/validation/monitorValidation.ts @@ -0,0 +1,121 @@ +import { z } from "zod"; +import { booleanCoercion } from "./shared.js"; +import { GeoContinents } from "@/types/geoCheck.js"; + +export const getMonitorByIdParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getMonitorByIdQueryValidation = z.object({ + status: booleanCoercion.optional(), + sortOrder: z.enum(["asc", "desc"]).optional(), + limit: z.coerce.number().optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + numToDisplay: z.coerce.number().optional(), + normalize: booleanCoercion.optional(), + continent: z.enum(GeoContinents).optional(), +}); + +export const getMonitorsByTeamIdParamValidation = z.object({}); + +export const getMonitorsByTeamIdQueryValidation = z.object({ + type: z + .union([ + z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]), + z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])), + ]) + .optional(), + filter: z.union([z.string(), z.literal(""), z.null()]).optional(), +}); + +export const getMonitorsWithChecksQueryValidation = z.object({ + limit: z.coerce.number().int().min(1).max(100).optional(), + page: z.coerce.number().int().min(0).optional(), + rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), + filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + field: z.string().optional(), + order: z.enum(["asc", "desc"]).optional(), + type: z + .union([ + z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]), + z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])), + ]) + .optional(), + explain: booleanCoercion.optional(), +}); + +export const getCertificateParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const createMonitorBodyValidation = z.object({ + _id: z.string().optional(), + name: z.string().min(1, "Name is required"), + description: z.union([z.string(), z.null(), z.literal("")]).optional(), + type: z.string().min(1, "Type is required"), + statusWindowSize: z.number().min(1).max(20).default(5), + statusWindowThreshold: z.number().min(1).max(100).default(60), + url: z.string().min(1, "URL is required"), + ignoreTlsErrors: z.boolean().default(false), + useAdvancedMatching: z.boolean().default(false), + port: z.number().optional(), + isActive: z.boolean().optional(), + interval: z.number().optional(), + cpuAlertThreshold: z.number().optional(), + memoryAlertThreshold: z.number().optional(), + diskAlertThreshold: z.number().optional(), + tempAlertThreshold: z.number().optional(), + notifications: z.array(z.string()).optional(), + secret: z.string().optional(), + jsonPath: z.union([z.string(), z.literal("")]).optional(), + expectedValue: z.union([z.string(), z.literal("")]).optional(), + matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + gameId: z.union([z.string(), z.literal("")]).optional(), + grpcServiceName: z.union([z.string(), z.literal("")]).default(""), + selectedDisks: z.array(z.string()).optional(), + group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), + geoCheckEnabled: z.boolean().optional(), + geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), + geoCheckInterval: z.number().min(300000).optional(), +}); + +export const editMonitorBodyValidation = z.object({ + name: z.string().optional(), + type: z.string().optional(), + url: z.string().optional(), + statusWindowSize: z.number().min(1).max(20).default(5), + statusWindowThreshold: z.number().min(1).max(100).default(60), + description: z.union([z.string(), z.null(), z.literal("")]).optional(), + interval: z.number().optional(), + notifications: z.array(z.string()).optional(), + secret: z.string().optional(), + ignoreTlsErrors: z.boolean().optional(), + useAdvancedMatching: z.boolean().optional(), + jsonPath: z.union([z.string(), z.literal("")]).optional(), + expectedValue: z.union([z.string(), z.literal("")]).optional(), + matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + port: z.number().min(1).max(65535).optional(), + cpuAlertThreshold: z.number().optional(), + memoryAlertThreshold: z.number().optional(), + diskAlertThreshold: z.number().optional(), + tempAlertThreshold: z.number().optional(), + gameId: z.union([z.string(), z.literal("")]).optional(), + grpcServiceName: z.union([z.string(), z.literal("")]).optional(), + selectedDisks: z.array(z.string()).optional(), + group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), + geoCheckEnabled: z.boolean().optional(), + geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), + geoCheckInterval: z.number().min(300000).optional(), +}); + +export const pauseMonitorParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getHardwareDetailsByIdParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getHardwareDetailsByIdQueryValidation = z.object({ + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), +}); diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts new file mode 100644 index 000000000..d974d873f --- /dev/null +++ b/server/src/validation/notificationValidation.ts @@ -0,0 +1,78 @@ +import { z } from "zod"; + +//**************************************** +// Notification Validations +//**************************************** + +export const createNotificationBodyValidation = z.discriminatedUnion("type", [ + // Email notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("email"), + address: z.email("Please enter a valid e-mail address"), + homeserverUrl: z.union([z.string(), z.literal("")]).optional(), + roomId: z.union([z.string(), z.literal("")]).optional(), + accessToken: z.union([z.string(), z.literal("")]).optional(), + }), + // Webhook notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("webhook"), + address: z.string().url("Please enter a valid Webhook URL"), + homeserverUrl: z.union([z.string(), z.literal("")]).optional(), + roomId: z.union([z.string(), z.literal("")]).optional(), + accessToken: z.union([z.string(), z.literal("")]).optional(), + }), + // Slack notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("slack"), + address: z.string().url("Please enter a valid Webhook URL"), + homeserverUrl: z.union([z.string(), z.literal("")]).optional(), + roomId: z.union([z.string(), z.literal("")]).optional(), + accessToken: z.union([z.string(), z.literal("")]).optional(), + }), + // Discord notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("discord"), + address: z.string().url("Please enter a valid Webhook URL"), + homeserverUrl: z.union([z.string(), z.literal("")]).optional(), + roomId: z.union([z.string(), z.literal("")]).optional(), + accessToken: z.union([z.string(), z.literal("")]).optional(), + }), + // PagerDuty notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("pager_duty"), + address: z.string().min(1, "PagerDuty integration key is required"), + homeserverUrl: z.union([z.string(), z.literal("")]).optional(), + roomId: z.union([z.string(), z.literal("")]).optional(), + accessToken: z.union([z.string(), z.literal("")]).optional(), + }), + // Matrix notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("matrix"), + address: z.union([z.string(), z.literal("")]).optional(), + homeserverUrl: z.string().url("Please enter a valid Homeserver URL"), + roomId: z.string().min(1, "Room ID is required"), + accessToken: z.string().min(1, "Access Token is required"), + }), +]); + +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/settingsValidation.ts b/server/src/validation/settingsValidation.ts new file mode 100644 index 000000000..ee2a2ee87 --- /dev/null +++ b/server/src/validation/settingsValidation.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +//**************************************** +// Settings Validations +//**************************************** + +export const updateAppSettingsBodyValidation = z + .object({ + checkTTL: z.union([z.number(), z.literal("")]).optional(), + systemEmailPort: z.union([z.number(), z.literal("")]).optional(), + + pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(), + language: z.union([z.string(), z.literal("")]).optional(), + timezone: z.union([z.string(), z.literal("")]).optional(), + systemEmailHost: z.union([z.string(), z.literal("")]).optional(), + systemEmailAddress: z.union([z.string(), z.literal("")]).optional(), + systemEmailPassword: z.union([z.string(), z.literal("")]).optional(), + systemEmailUser: z.union([z.string(), z.literal("")]).optional(), + systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(), + systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(), + + showURL: z.boolean().optional(), + systemEmailSecure: z.boolean().optional(), + systemEmailPool: z.boolean().optional(), + systemEmailIgnoreTLS: z.boolean().optional(), + systemEmailRequireTLS: z.boolean().optional(), + systemEmailRejectUnauthorized: z.boolean().optional(), + + globalThresholds: z + .object({ + cpu: z.union([z.number().min(1).max(100), z.literal("")]).optional(), + memory: z.union([z.number().min(1).max(100), z.literal("")]).optional(), + disk: z.union([z.number().min(1).max(100), z.literal("")]).optional(), + temperature: z.union([z.number().min(1).max(150), z.literal("")]).optional(), + }) + .optional(), + }) + .strip(); diff --git a/server/src/validation/shared.ts b/server/src/validation/shared.ts new file mode 100644 index 000000000..28fd13c82 --- /dev/null +++ b/server/src/validation/shared.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { type UserRole } from "@/types/user.js"; + +export const passwordPattern = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/]+$/; + +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 ()." + ); + +export const lowercaseEmailValidation = z.email().transform((val) => val.toLowerCase()); + +export const booleanCoercion = z.preprocess((val) => { + if (val === "true" || val === true) return true; + if (val === "false" || val === false) return false; + return val; // Let Zod validation handle invalid values +}, z.boolean()); + +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..b4b5e270c --- /dev/null +++ b/server/src/validation/statusPageValidation.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import { booleanCoercion } from "./shared.js"; + +//**************************************** +// Status Page Validations +//**************************************** + +export const getStatusPageParamValidation = z.object({ + url: z.string().min(1, "URL is required"), +}); + +export const getStatusPageQueryValidation = z.object({ + type: z.literal("uptime"), + timeFrame: z.coerce.number().optional(), +}); + +export const createStatusPageBodyValidation = z + .object({ + type: z.literal("uptime"), + companyName: z.string().min(1, "Company name is required"), + url: z.string().regex(/^[a-zA-Z0-9_-]+$/, { + message: "URL can only contain letters, numbers, underscores, and hyphens", + }), + timezone: z.string().optional(), + color: z.string().optional(), + monitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/, "Must be a valid monitor ID")).min(1, "At least one monitor is required"), + subMonitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/)).optional(), + deleteSubmonitors: z.boolean().optional(), + isPublished: booleanCoercion, + showCharts: booleanCoercion.optional(), + showUptimePercentage: booleanCoercion, + showAdminLoginLink: booleanCoercion.optional(), + removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(), + }) + .strip(); + +export const imageValidation = z + .object({ + fieldname: z.string().min(1, "Field name is required"), + originalname: z.string().min(1, "Original name is required"), + encoding: z.string().min(1, "Encoding is required"), + mimetype: z.enum(["image/jpeg", "image/png", "image/jpg"], { + message: "File must be a valid image (jpeg, jpg, or png)", + }), + size: z.number().max(3145728, "File size must be less than 3MB"), + buffer: z.instanceof(Buffer, { message: "Buffer is required" }), + destination: z.string().optional(), + filename: z.string().optional(), + path: z.string().optional(), + }) + .refine((data) => data.buffer, { + message: "Image file is required", + }); 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(), +});