From 2fdd5a72f038e046f95e7cf295a720f4e42ed36d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 11:18:59 -0700 Subject: [PATCH 01/30] type authController correctly, update services accordingly --- server/src/controllers/authController.ts | 69 ++++++++------- server/src/controllers/controllerUtils.ts | 4 +- server/src/service/business/userService.ts | 88 ++++++++++---------- server/src/service/system/settingsService.ts | 6 +- 4 files changed, 89 insertions(+), 78 deletions(-) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 3f837f632..cdca8fb52 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -1,7 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { AppError } from "@/utils/AppError.js"; -import { requireTeamId, requireUserRoles } from "./controllerUtils.js"; -import type { UserRole } from "@/types/user.js"; +import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { registrationBodyValidation, @@ -20,15 +19,16 @@ import { editSuperadminUserByIdBodyValidation, editUserPasswordByIdBodyValidation, } from "@/validation/userValidation.js"; +import { IUserService } from "@/service/index.js"; const SERVICE_NAME = "authController"; class AuthController { static SERVICE_NAME = SERVICE_NAME; - private userService: any; + private userService: IUserService; - constructor(userService: any) { + constructor(userService: IUserService) { this.userService = userService; } @@ -41,11 +41,12 @@ class AuthController { const newUser = req.body.user; const newUserToken = req.body.token; if (newUser?.email) { - newUser.email = newUser.email.toLowerCase(); + const newUserEmail = requireUserEmail(newUser.email); + newUser.email = newUserEmail.toLowerCase(); } registrationBodyValidation.parse(newUser); - const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req?.file ?? null); res.status(200).json({ success: true, msg: "User registered successfully", @@ -62,11 +63,11 @@ class AuthController { if (userData?.email) { userData.email = userData.email.toLowerCase(); } - createUserBodyValidation.parse(userData); + const validatedBody = createUserBodyValidation.parse(userData); const teamId = requireTeamId(req.user?.teamId); - const actorRoles = requireUserRoles(req.user?.role) as UserRole[]; - const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file); + const actorRoles = requireUserRoles(req.user?.role); + const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); res.status(201).json({ success: true, msg: "User created successfully", @@ -101,7 +102,9 @@ class AuthController { editUser = async (req: Request, res: Response, next: NextFunction) => { try { const validatedBody = editUserBodyValidation.parse(req.body); - const updatedUser = await this.userService.editUser(validatedBody, req.file, req.user); + const userId = requireUserId(req.user?.id); + const userEmail = requireUserEmail(req.user?.email); + const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail); res.status(200).json({ success: true, @@ -170,7 +173,15 @@ class AuthController { deleteUser = async (req: Request, res: Response, next: NextFunction) => { try { - await this.userService.deleteUser(req.user); + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const roles = requireUserRoles(req.user?.role); + + await this.userService.deleteUser({ + userId, + teamId, + roles, + }); return res.status(200).json({ success: true, msg: "User deleted successfully", @@ -182,9 +193,12 @@ class AuthController { deleteUserById = async (req: Request, res: Response, next: NextFunction) => { try { - getUserByIdParamValidation.parse(req.params); - const targetUserId = req.params.userId; - await this.userService.deleteUserById(req.user, targetUserId); + const validatedParams = getUserByIdParamValidation.parse(req.params); + const targetUserId = validatedParams.userId; + const actorId = requireUserId(req.user?.id); + const actorTeamId = requireTeamId(req.user?.teamId); + const actorRoles = requireUserRoles(req.user?.role); + await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId }); return res.status(200).json({ success: true, msg: "User removed successfully", @@ -231,24 +245,19 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; + const roles = requireUserRoles(req.user?.role); + const actorId = requireUserId(req.user?.id); if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId as string; - const user = { ...req.body }; - - editUserByIdParamValidation.parse(req.params); + const validatedParams = editUserByIdParamValidation.parse(req.params); // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user?.id) { - editSuperadminUserByIdBodyValidation.parse(req.body); - } else { - editUserByIdBodyValidation.parse(req.body); - } + const validatedBody = + validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body); - await this.userService.editUserById(userId, user); + await this.userService.editUserById(validatedParams.userId, validatedBody); return res.status(200).json({ success: true, msg: "ok" }); } catch (error) { next(error); @@ -257,16 +266,14 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; + const roles = requireUserRoles(req.user?.role); if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId as string; - editUserByIdParamValidation.parse(req.params); - editUserPasswordByIdBodyValidation.parse(req.body); - const updatedPassword = req.body.password; - await this.userService.setPasswordByUserId(userId, updatedPassword); + const validatedParams = editUserByIdParamValidation.parse(req.params); + const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); + await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); return res.status(200).json({ success: true, msg: "Password reset successfully" }); } catch (error) { next(error); diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index 99c60807e..e5c852591 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,5 +1,5 @@ import { AppError } from "@/utils/AppError.js"; -import { type MonitorType, MonitorTypes } from "@/types/index.js"; +import { type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { const monitorUrl = new URL(monitor.url); @@ -112,7 +112,7 @@ const requireUserEmail = (userEmail?: string): string => { return userEmail; }; -export const requireUserRoles = (userRoles?: string[]): string[] => { +export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => { if (!userRoles || userRoles.length === 0) { throw new AppError({ message: "User roles are required", status: 400 }); } diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 3337e491d..e5b6f9e68 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -12,24 +12,31 @@ import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; import { IEmailService } from "@/service/infrastructure/emailService.js"; -import { ISettingsService } from "@/service/system/settingsService.js"; +import { EnvConfig, ISettingsService } from "@/service/system/settingsService.js"; import { ILogger } from "@/utils/logger.js"; import jwt from "jsonwebtoken"; +import crypto from "crypto"; +type CryptoType = typeof crypto; type JwtType = typeof jwt; const SERVICE_NAME = "userService"; export interface IUserService { - issueToken(payload: Partial, appSettings: { jwtTTL?: string; jwtSecret?: string }): string; - registerUser(user: Partial, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>; - createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: unknown): Promise; + issueToken(payload: Partial, appSettings: EnvConfig): string; + registerUser(user: Partial, inviteToken: string, file: Express.Multer.File | null): Promise<{ user: User; token: string }>; + createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise; loginUser(email: string, password: string): Promise<{ user: User; token: string }>; - editUser(updates: Partial, file: unknown, currentUser: User): Promise; + editUser( + updates: Partial, + file: Express.Multer.File | null, + currentUserId: string, + currentUserEmail: string + ): Promise; checkSuperadminExists(): Promise; requestRecovery(email: string): Promise; validateRecovery(recoveryToken: string): Promise; resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>; - deleteUser(user: User): Promise; - deleteUserById(actor: User, targetUserId: string): Promise; + deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise; + deleteUserById(params: { actorId: string; actorTeamId: string; actorRoles: UserRole[]; targetUserId: string }): Promise; getAllUsers(): Promise; getUserById(roles: UserRole[], userId: string): Promise; editUserById(userId: string, patch: Partial): Promise; @@ -49,7 +56,7 @@ export class UserService implements IUserService { private logger: ILogger; private jwt: JwtType; private jobQueue: ISuperSimpleQueue; - private crypto: any; + private crypto: CryptoType; private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; @@ -71,7 +78,7 @@ export class UserService implements IUserService { settingsRepository, teamsRepository, }: { - crypto: any; + crypto: CryptoType; emailService: IEmailService; settingsService: ISettingsService; logger: ILogger; @@ -102,14 +109,14 @@ export class UserService implements IUserService { return UserService.SERVICE_NAME; } - issueToken = (payload: any, appSettings: any) => { + issueToken = (payload: Partial, appSettings: EnvConfig) => { const tokenTTL = appSettings?.jwtTTL ?? "2h"; const tokenSecret = appSettings?.jwtSecret; const payloadData = payload; return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user: Partial, inviteToken: string, file: any) => { + registerUser = async (user: Partial, inviteToken: string, file: Express.Multer.File | null) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.usersRepository.findSuperAdmin(); @@ -179,7 +186,7 @@ export class UserService implements IUserService { return { user: newUser, token }; }; - createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: any) => { + createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null) => { // Validate that the creator can assign the requested roles const targetRoles = userData.role ?? []; for (const targetRole of targetRoles) { @@ -239,14 +246,19 @@ export class UserService implements IUserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates: Partial, file: any, currentUser: any) => { + editUser = async ( + updates: Partial, + file: Express.Multer.File | null, + currentUserId: string, + currentUserEmail: string + ) => { // Change Password check if (updates?.password && updates?.newPassword) { // Get user's email // Add user email to body for DB operation - updates.email = currentUser.email; + updates.email = currentUserEmail; // Get user - const user = await this.usersRepository.findByEmail(currentUser.email); + const user = await this.usersRepository.findByEmail(currentUserEmail); // Compare passwords const match = await bcrypt.compare(updates?.password, user.password); // If not a match, throw a 403 @@ -259,7 +271,7 @@ export class UserService implements IUserService { delete updates.newPassword; } - return await this.usersRepository.updateById(currentUser.id, updates, file); + return await this.usersRepository.updateById(currentUserId, updates, file); }; checkSuperadminExists = async () => { @@ -319,24 +331,7 @@ export class UserService implements IUserService { return { user: existingUser, token }; }; - deleteUser = async (user: User) => { - const email = user?.email; - if (!email) { - throw new AppError({ message: "No email in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); - } - - const teamId = user?.teamId; - const userId = user?.id; - - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); - } - - if (!userId) { - throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); - } - - const roles = user?.role; + deleteUser = async ({ userId, teamId, roles }: { userId: string; teamId: string; roles: UserRole[] }) => { if (roles.includes("demo")) { throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 }); } @@ -358,14 +353,24 @@ export class UserService implements IUserService { await this.usersRepository.deleteById(userId); }; - deleteUserById = async (actor: User, targetUserId: string) => { - if (actor.id === targetUserId) { + deleteUserById = async ({ + actorId, + actorRoles, + actorTeamId, + targetUserId, + }: { + actorId: string; + actorTeamId: string; + actorRoles: UserRole[]; + targetUserId: string; + }) => { + if (actorId === targetUserId) { throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); } const targetUser = await this.usersRepository.findById(targetUserId); - if (targetUser.teamId !== actor.teamId) { + if (targetUser.teamId !== actorTeamId) { throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 }); } @@ -373,7 +378,6 @@ export class UserService implements IUserService { throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); } - const actorRoles = actor.role; const targetRoles = targetUser.role; // Check actor can manage all of target's roles @@ -392,7 +396,7 @@ export class UserService implements IUserService { await this.usersRepository.deleteById(targetUserId); this.logger.info({ - message: `User ${targetUserId} deleted by ${actor.id}`, + message: `User ${targetUserId} deleted by ${actorId}`, service: SERVICE_NAME, method: "deleteUserById", }); @@ -402,7 +406,7 @@ export class UserService implements IUserService { return await this.usersRepository.findAll(); }; - getUserById = async (roles: any, userId: any) => { + getUserById = async (roles: UserRole[], userId: string) => { if (!roles.includes("superadmin") && !roles.includes("admin")) { throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 }); } @@ -411,11 +415,11 @@ export class UserService implements IUserService { return user; }; - editUserById = async (userId: any, patch: Partial) => { + editUserById = async (userId: string, patch: Partial) => { await this.usersRepository.updateById(userId, patch, null); }; - setPasswordByUserId = async (userId: any, password: string) => { + setPasswordByUserId = async (userId: string, password: string) => { const hashedPassword = this.hashPassword(password); const updatedUser = await this.usersRepository.updateById(userId, { password: hashedPassword }, null); return updatedUser; diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 1d75c0a36..ac4dfc425 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -2,12 +2,12 @@ import { ISettingsRepository } from "@/repositories/index.js"; import { Settings } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; import { ValidatedEnv } from "@/validation/envValidation.js"; - +import type { StringValue } from "ms"; const SERVICE_NAME = "SettingsService"; export type EnvConfig = { jwtSecret: string; - jwtTTL: string; + jwtTTL: StringValue; nodeEnv: string; logLevel: string; clientHost: string; @@ -30,7 +30,7 @@ export class SettingsService implements ISettingsService { this.settingsRepository = settingsRepository; this.settings = { jwtSecret: env.JWT_SECRET, - jwtTTL: env.TOKEN_TTL, + jwtTTL: env.TOKEN_TTL as StringValue, nodeEnv: env.NODE_ENV, logLevel: env.LOG_LEVEL, clientHost: env.CLIENT_HOST, From 562d723fcf4afa38c758c69302e07bb3cf144699 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 11:25:55 -0700 Subject: [PATCH 02/30] remove redundant checks, use validated body --- server/src/controllers/authController.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index cdca8fb52..dbe431a5b 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -44,9 +44,9 @@ class AuthController { const newUserEmail = requireUserEmail(newUser.email); newUser.email = newUserEmail.toLowerCase(); } - registrationBodyValidation.parse(newUser); + const validatedBody = registrationBodyValidation.parse(newUser); - const { user, token } = await this.userService.registerUser(newUser, newUserToken, req?.file ?? null); + const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null); res.status(200).json({ success: true, msg: "User registered successfully", @@ -224,17 +224,8 @@ class AuthController { getUserById = async (req: Request, res: Response, next: NextFunction) => { try { getUserByIdParamValidation.parse(req.params); - const userId = req?.params?.userId; - const roles = req?.user?.role; - - if (!userId) { - throw new Error("No user ID in request"); - } - - if (!roles || roles.length === 0) { - throw new Error("No roles in request"); - } - + const userId = requireUserId(req.user?.id); + const roles = requireUserRoles(req.user?.role); const user = await this.userService.getUserById(roles, userId); return res.status(200).json({ success: true, msg: "ok", data: user }); @@ -248,7 +239,7 @@ class AuthController { const roles = requireUserRoles(req.user?.role); const actorId = requireUserId(req.user?.id); - if (!roles || !roles.includes("superadmin")) { + if (!roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } @@ -267,7 +258,7 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { const roles = requireUserRoles(req.user?.role); - if (!roles || !roles.includes("superadmin")) { + if (!roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } From a15b9837c670ca891119318828e1d70b92ee7415 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 11:29:40 -0700 Subject: [PATCH 03/30] fix getUserById --- server/src/controllers/authController.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index dbe431a5b..6eb49bbdd 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -223,10 +223,9 @@ class AuthController { getUserById = async (req: Request, res: Response, next: NextFunction) => { try { - getUserByIdParamValidation.parse(req.params); - const userId = requireUserId(req.user?.id); - const roles = requireUserRoles(req.user?.role); - const user = await this.userService.getUserById(roles, userId); + const validatedParams = getUserByIdParamValidation.parse(req.params); + const actorRoles = requireUserRoles(req.user?.role); + const user = await this.userService.getUserById(actorRoles, validatedParams.userId); return res.status(200).json({ success: true, msg: "ok", data: user }); } catch (error) { @@ -236,10 +235,10 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = requireUserRoles(req.user?.role); + const actorRoles = requireUserRoles(req.user?.role); const actorId = requireUserId(req.user?.id); - if (!roles.includes("superadmin")) { + if (!actorRoles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } @@ -257,8 +256,8 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = requireUserRoles(req.user?.role); - if (!roles.includes("superadmin")) { + const actorRoles = requireUserRoles(req.user?.role); + if (!actorRoles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } From 2884ab524d2f60350a9535f4d64fe0821812e534 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 11:35:05 -0700 Subject: [PATCH 04/30] type fetchMonitorCertificate --- server/src/controllers/controllerUtils.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index e5c852591..173790cda 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,11 +1,12 @@ import { AppError } from "@/utils/AppError.js"; -import { type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; +import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; +type SslCertificate = { valid: boolean; validFrom: string; validTo: string; daysRemaining: number; validFor?: string[] }; +type SslChecker = (host: string, options?: Record) => Promise; -const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { +const fetchMonitorCertificate = async (checker: SslChecker, monitor: Monitor): Promise => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; - const cert = await sslChecker(hostname); - // Throw an error if no cert or if cert.validTo is not present + const cert = await checker(hostname); if (cert?.validTo === null || cert?.validTo === undefined) { throw new Error("Certificate not found"); } From 84cac870d5b34fefa9520ddc4dfd013075c50f1e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Tue, 10 Mar 2026 19:04:04 -0400 Subject: [PATCH 05/30] fix: set DNS cache TTL to prevent stale lookups CacheableLookup defaults to maxTtl=Infinity, meaning successful DNS lookups are cached forever until process restart. This causes monitors to fail permanently if DNS changes or if a resolver outage causes ENOTFOUND errors to persist after recovery. Set maxTtl=300s (5 min) for successful lookups and errorTtl=30s for failed lookups so the cache recovers automatically. --- server/src/service/infrastructure/network/HttpProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index 42fa39a40..a947727fd 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -15,7 +15,7 @@ export class HttpProvider implements IStatusProvider { private got: Got, private advancedMatcher: AdvancedMatcher ) { - const cacheable = new CacheableLookup(); + const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 }); this.got = got.extend({ dnsCache: cacheable, timeout: { From 943ef9205370b015e8ae1ca72dbd35529e2b0213 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 11 Mar 2026 01:31:05 -0400 Subject: [PATCH 06/30] fix: rename "breached" status to "exceeded" for clarity The word "breached" in infrastructure monitoring was confusing users as it implied a security breach. Renamed to "exceeded" which better describes threshold violations (e.g. "threshold exceeded"). ## Changes - Renamed monitor status value from "breached" to "exceeded" across server types, status service, queue helper, notification builder, email subject lines, and MongoDB aggregation queries - Renamed client types, status box/label components, translation key, and infrastructure page props to match - Added DB migration (0005) to update existing monitors with the old status value --- .../Components/design-elements/StatusBox.tsx | 4 +-- .../design-elements/StatusLabel.tsx | 4 +-- .../monitors/HeaderMonitorsSummary.tsx | 8 +++--- .../Pages/Infrastructure/Monitors/index.tsx | 2 +- client/src/Types/Monitor.ts | 4 +-- client/src/Utils/MonitorUtils.ts | 2 +- client/src/locales/en.json | 2 +- .../0005_renameBreachedToExceeded.ts | 25 +++++++++++++++++++ server/src/db/migration/index.ts | 2 ++ .../monitors/MongoMonitorsRepository.ts | 6 ++--- .../SuperSimpleQueueHelper.ts | 6 ++--- .../notificationMessageBuilder.ts | 6 ++--- .../notificationProviders/email.ts | 2 +- .../service/infrastructure/statusService.ts | 22 ++++++++-------- server/src/types/monitor.ts | 4 +-- 15 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 server/src/db/migration/0005_renameBreachedToExceeded.ts diff --git a/client/src/Components/design-elements/StatusBox.tsx b/client/src/Components/design-elements/StatusBox.tsx index 96a9d9fbf..895709b04 100644 --- a/client/src/Components/design-elements/StatusBox.tsx +++ b/client/src/Components/design-elements/StatusBox.tsx @@ -139,12 +139,12 @@ export const InitializingStatusBox = ({ n }: { n: number }) => { /> ); }; -export const BreachedStatusBox = ({ n }: { n: number }) => { +export const ExceededStatusBox = ({ n }: { n: number }) => { const theme = useTheme(); const { t } = useTranslation(); return ( diff --git a/client/src/Components/design-elements/StatusLabel.tsx b/client/src/Components/design-elements/StatusLabel.tsx index d50058378..e9c92a490 100644 --- a/client/src/Components/design-elements/StatusLabel.tsx +++ b/client/src/Components/design-elements/StatusLabel.tsx @@ -21,8 +21,8 @@ export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProp return t("pages.common.monitors.status.up"); } else if (status === "down") { return t("pages.common.monitors.status.down"); - } else if (status === "breached") { - return t("pages.common.monitors.status.breached"); + } else if (status === "exceeded") { + return t("pages.common.monitors.status.exceeded"); } else if (status === "maintenance") { return t("pages.common.monitors.status.maintenance"); } else if (status === "paused") { diff --git a/client/src/Components/monitors/HeaderMonitorsSummary.tsx b/client/src/Components/monitors/HeaderMonitorsSummary.tsx index 1d130209d..07edc406a 100644 --- a/client/src/Components/monitors/HeaderMonitorsSummary.tsx +++ b/client/src/Components/monitors/HeaderMonitorsSummary.tsx @@ -3,7 +3,7 @@ import { DownStatusBox, PausedStatusBox, InitializingStatusBox, - BreachedStatusBox, + ExceededStatusBox, } from "@/Components/design-elements"; import Stack from "@mui/material/Stack"; @@ -12,12 +12,12 @@ import { useTheme } from "@mui/material"; interface MonitorsSummaryProps { summary: MonitorsSummary | null; - showBreached?: boolean; + showExceeded?: boolean; } export const HeaderMonitorsSummary = ({ summary, - showBreached = false, + showExceeded = false, }: MonitorsSummaryProps) => { const theme = useTheme(); return ( @@ -27,7 +27,7 @@ export const HeaderMonitorsSummary = ({ > - {showBreached && } + {showExceeded && } diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx index bdf477613..53c3f16e0 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.tsx +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -135,7 +135,7 @@ const InfrastructureMonitors = () => { /> { if (status === "down") { return "error"; } - if (status === "breached") { + if (status === "exceeded") { return "error"; } return "warning"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 4c4965aec..85357772e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -401,7 +401,7 @@ }, "status": { "down": "down", - "breached": "breached", + "exceeded": "threshold exceeded", "initializing": "initializing", "maintenance": "maintenance", "paused": "paused", diff --git a/server/src/db/migration/0005_renameBreachedToExceeded.ts b/server/src/db/migration/0005_renameBreachedToExceeded.ts new file mode 100644 index 000000000..ea2f9d24e --- /dev/null +++ b/server/src/db/migration/0005_renameBreachedToExceeded.ts @@ -0,0 +1,25 @@ +import { MonitorModel } from "../models/Monitor.js"; +import { logger } from "@/utils/logger.js"; + +/** + * Rename monitor status "breached" to "exceeded" for clarity. + * The word "breached" was confusing users as it implied a security breach. + */ +export async function renameBreachedToExceeded(): Promise { + const SERVICE_NAME = "Migration:RenameBreachedToExceeded"; + + try { + logger.info({ service: SERVICE_NAME, message: "Starting status rename: breached -> exceeded" }); + + const result = await MonitorModel.updateMany({ status: "breached" }, { $set: { status: "exceeded" } }); + + logger.info({ + service: SERVICE_NAME, + message: `Updated ${result.modifiedCount} monitors from 'breached' to 'exceeded'`, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error({ service: SERVICE_NAME, message: `Error renaming status: ${errorMessage}` }); + throw error; + } +} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index 3b7b45ada..61bf358eb 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -2,6 +2,7 @@ import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshol import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js"; +import { renameBreachedToExceeded } from "./0005_renameBreachedToExceeded.js"; import MigrationModel from "../models/Migration.js"; type MigrationEntry = { @@ -14,6 +15,7 @@ const migrations: MigrationEntry[] = [ { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, + { name: "0005_renameBreachedToExceeded", execute: renameBreachedToExceeded }, ]; const runMigrations = async (logger?: { info: Function; error: Function }) => { diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a3b40f459..a075d42b9 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -262,9 +262,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { $cond: [{ $eq: ["$status", "maintenance"] }, 1, 0], }, }, - breachedMonitors: { + exceededMonitors: { $sum: { - $cond: [{ $eq: ["$status", "breached"] }, 1, 0], + $cond: [{ $eq: ["$status", "exceeded"] }, 1, 0], }, }, }, @@ -281,7 +281,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { pausedMonitors: 0, initializingMonitors: 0, maintenanceMonitors: 0, - breachedMonitors: 0, + exceededMonitors: 0, } ); }; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index b6908127b..e5d746635 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -440,14 +440,14 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { decision.shouldSendNotification = true; decision.incidentReason = "status_down"; decision.notificationReason = "status_change"; - } else if (monitor.status === "breached") { + } else if (monitor.status === "exceeded") { // Hardware monitor exceeded thresholds decision.shouldCreateIncident = true; decision.shouldSendNotification = true; decision.incidentReason = "threshold_breach"; decision.notificationReason = "threshold_breach"; - } else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "breached")) { - // Monitor recovered from down or breached state + } else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "exceeded")) { + // Monitor recovered from down or exceeded state decision.shouldResolveIncident = true; decision.shouldSendNotification = true; decision.notificationReason = "status_change"; diff --git a/server/src/service/infrastructure/notificationMessageBuilder.ts b/server/src/service/infrastructure/notificationMessageBuilder.ts index 3d16acf19..934163b2a 100644 --- a/server/src/service/infrastructure/notificationMessageBuilder.ts +++ b/server/src/service/infrastructure/notificationMessageBuilder.ts @@ -145,9 +145,9 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { - const title = `Threshold Breach: ${monitor.name}`; - const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`; - const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`]; + const title = `Threshold Exceeded: ${monitor.name}`; + const summary = `Monitor "${monitor.name}" has exceeded one or more thresholds.`; + const details = [`URL: ${monitor.url}`, `Status: Threshold exceeded`, `Type: ${monitor.type}`]; const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse); diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index c14d59a85..338ceb619 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -84,7 +84,7 @@ export class EmailProvider implements INotificationProvider { case "monitor_up": return `Monitor ${message.monitor.name} is back up`; case "threshold_breach": - return `Monitor ${message.monitor.name} threshold breached`; + return `Monitor ${message.monitor.name} threshold exceeded`; case "threshold_resolved": return `Monitor ${message.monitor.name} thresholds resolved`; default: diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index badcf78ec..ef8c6125e 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -292,7 +292,7 @@ export class StatusService implements IStatusService { temp: tempBreach, }; - // Update counters: decrement if breached, reset to 5 if not breached + // Update counters: decrement if exceeded, reset to 5 if not exceeded if (cpuBreach) { monitor.cpuAlertCounter = Math.max(0, monitor.cpuAlertCounter - 1); } else { @@ -321,23 +321,23 @@ export class StatusService implements IStatusService { const anyCounterZero = monitor.cpuAlertCounter === 0 || monitor.memoryAlertCounter === 0 || monitor.diskAlertCounter === 0 || monitor.tempAlertCounter === 0; - const anyThresholdBreached = cpuBreach || memoryBreach || diskBreach || tempBreach; + const anyThresholdExceeded = cpuBreach || memoryBreach || diskBreach || tempBreach; const allThresholdsNormal = !cpuBreach && !memoryBreach && !diskBreach && !tempBreach; - // Update monitor status based on threshold breach state + // Update monitor status based on threshold exceeded state if (newStatus !== "down") { // Don't override "down" status - service unreachable takes precedence // Check current monitor status, not newStatus for comparison - if (anyCounterZero && anyThresholdBreached && monitor.status !== "breached") { - // Initial breach: counter hit zero, change status to breached - newStatus = "breached"; + if (anyCounterZero && anyThresholdExceeded && monitor.status !== "exceeded") { + // Threshold exceeded: counter hit zero, change status to exceeded + newStatus = "exceeded"; statusChanged = true; - } else if (anyCounterZero && anyThresholdBreached && monitor.status === "breached") { - // Already breached, keep status but don't mark as changed - newStatus = "breached"; + } else if (anyCounterZero && anyThresholdExceeded && monitor.status === "exceeded") { + // Already exceeded, keep status but don't mark as changed + newStatus = "exceeded"; // statusChanged remains false - } else if (allThresholdsNormal && monitor.status === "breached") { - // All thresholds returned to normal, recover from breached state + } else if (allThresholdsNormal && monitor.status === "exceeded") { + // All thresholds returned to normal, recover from exceeded state newStatus = "up"; statusChanged = true; } diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 89740162b..62a0d3aa6 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -9,7 +9,7 @@ export type MonitorType = (typeof MonitorTypes)[number]; export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type); -export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; +export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "exceeded"] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; @@ -64,7 +64,7 @@ export interface MonitorsSummary { pausedMonitors: number; initializingMonitors: number; maintenanceMonitors: number; - breachedMonitors: number; + exceededMonitors: number; } export interface MonitorsWithChecksByTeamIdResult { From f6d9b7566bc37884d62726b92f553e81d5080c68 Mon Sep 17 00:00:00 2001 From: Gorkem Cetin <167266851+gorkem-bwl@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:47:48 -0400 Subject: [PATCH 07/30] Remove project references from README Removed references to other BlueWave projects from the README. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 07c0e84bd..73227a5e4 100644 --- a/README.md +++ b/README.md @@ -175,9 +175,3 @@ Here's how you can contribute: Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features). - -Also check other developer and contributor-friendly projects of BlueWave: - -- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform. -- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom. -- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners. From 85d2d34a90e1adfc73bf19de1ca3b123d9e1fe4e Mon Sep 17 00:00:00 2001 From: Gorkem Cetin <167266851+gorkem-bwl@users.noreply.github.com> Date: Wed, 11 Mar 2026 02:35:54 -0400 Subject: [PATCH 08/30] Fix links to Checkmate documentation and user guide Updated links to documentation and user guides in README. --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 73227a5e4..786e6102b 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs. ## User's guide -Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :) +Usage instructions can be found [here](https://checkmate.so/docs). ## Installation -See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). +See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs). Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. @@ -157,12 +157,11 @@ Here's how you can contribute: 0. Star this repo :) 1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag. -2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview). -3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. -4. Open an issue if you believe you've encountered a bug. -5. Check for good-first-issue's if you are a newcomer. -6. Make a pull request to add new features/make quality-of-life improvements/fix bugs. -7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). +2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. +3. Open an issue if you believe you've encountered a bug. +4. Check for good-first-issue's if you are a newcomer. +5. Make a pull request to add new features/make quality-of-life improvements/fix bugs. +6. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). From 1ad39fcac6e074f7a0673ad833d8b9097e43e360 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Wed, 11 Mar 2026 12:42:57 -0400 Subject: [PATCH 09/30] fix: empty email body due to TSC ignoring .mjml templates --- docker/dist-mono/server.Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile index 84b7127ed..aa7f898f6 100644 --- a/docker/dist-mono/server.Dockerfile +++ b/docker/dist-mono/server.Dockerfile @@ -24,6 +24,8 @@ RUN npm install RUN npm run build +RUN cp -r ./src/templates ./dist/templates + COPY --from=frontend-build /app/client/dist ./public RUN chmod +x ./scripts/inject-vars.sh From bcff3e15a874be6ee560a4270cf4d78ff23c2071 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Wed, 11 Mar 2026 13:34:25 -0400 Subject: [PATCH 10/30] fix: updating build script to add .mjml templates --- docker/dist-mono/server.Dockerfile | 2 -- server/package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile index aa7f898f6..84b7127ed 100644 --- a/docker/dist-mono/server.Dockerfile +++ b/docker/dist-mono/server.Dockerfile @@ -24,8 +24,6 @@ RUN npm install RUN npm run build -RUN cp -r ./src/templates ./dist/templates - COPY --from=frontend-build /app/client/dist ./public RUN chmod +x ./scripts/inject-vars.sh diff --git a/server/package.json b/server/package.json index 7bceee766..88f90c68a 100755 --- a/server/package.json +++ b/server/package.json @@ -8,7 +8,7 @@ "test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand", "dev": "nodemon --exec tsx src/index.js", "start": "node --watch ./dist/index.js", - "build": "tsc && tsc-alias", + "build": "tsc && tsc-alias && cp -r src/templates dist/templates", "lint": "eslint .", "lint-fix": "eslint --fix .", "format": "prettier --write .", From cb5316e6f568ad615f96364f2554715e59816292 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 10:48:12 -0700 Subject: [PATCH 11/30] fix ssl-checker type --- server/src/controllers/controllerUtils.ts | 6 +++--- server/src/types/ssl-checker.d.ts | 24 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 server/src/types/ssl-checker.d.ts diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index 173790cda..f8447fd59 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,9 +1,9 @@ import { AppError } from "@/utils/AppError.js"; import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; -type SslCertificate = { valid: boolean; validFrom: string; validTo: string; daysRemaining: number; validFor?: string[] }; -type SslChecker = (host: string, options?: Record) => Promise; +import sslChecker, { SSLDetails } from "ssl-checker"; +type SSLCheckerType = typeof sslChecker; -const fetchMonitorCertificate = async (checker: SslChecker, monitor: Monitor): Promise => { +const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; const cert = await checker(hostname); diff --git a/server/src/types/ssl-checker.d.ts b/server/src/types/ssl-checker.d.ts new file mode 100644 index 000000000..bcbc39858 --- /dev/null +++ b/server/src/types/ssl-checker.d.ts @@ -0,0 +1,24 @@ +declare module "ssl-checker" { + import { Agent } from "https"; + + export interface SSLDetails { + daysRemaining: number; + valid: boolean; + validFrom: string; + validTo: string; + validFor: string[]; + } + + export interface SSLOptions { + method?: "GET" | "HEAD"; + port?: number; + agent?: Agent; + rejectUnauthorized?: boolean; + validateSubjectAltName?: boolean; + } + + function sslChecker(hostname: string, options?: SSLOptions): Promise; + + // Change 'export default' to 'export =' to allow it to be callable + export = sslChecker; +} From f69b7f6a35864c9ca3c49e2290d4ec7a70ffb152 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 10:50:10 -0700 Subject: [PATCH 12/30] properly type diagnosticController --- server/src/controllers/diagnosticController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/controllers/diagnosticController.ts b/server/src/controllers/diagnosticController.ts index b4d59ceaf..19326d3b3 100755 --- a/server/src/controllers/diagnosticController.ts +++ b/server/src/controllers/diagnosticController.ts @@ -1,12 +1,13 @@ +import { IDiagnosticService } from "@/service/index.js"; import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "diagnosticController"; class DiagnosticController { static SERVICE_NAME = SERVICE_NAME; - private diagnosticService: any; + private diagnosticService: IDiagnosticService; - constructor(diagnosticService: any) { + constructor(diagnosticService: IDiagnosticService) { this.diagnosticService = diagnosticService; } From eafd97b9ade8bb95fae89b15838cf3d5a0cc3150 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 11:10:38 -0700 Subject: [PATCH 13/30] controller, service, validation --- server/src/controllers/incidentController.ts | 36 +++++------- .../src/service/business/incidentService.ts | 58 +++++++++---------- server/src/validation/incidentValidation.ts | 20 +++++++ server/src/validation/index.ts | 1 + 4 files changed, 64 insertions(+), 51 deletions(-) create mode 100644 server/src/validation/incidentValidation.ts diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 0eaba7173..4e6242184 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -1,35 +1,30 @@ import { AppError } from "@/utils/AppError.js"; import { Request, Response, NextFunction } from "express"; import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js"; +import { IIncidentService } from "@/service/index.js"; +import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js"; -const SERVICE_NAME = "incidentController"; - +const SERVICE_NAME = "IncidentController"; class IncidentController { - static SERVICE_NAME = SERVICE_NAME; - - private incidentService: any; - constructor(incidentService: any) { + private incidentService: IIncidentService; + constructor(incidentService: IIncidentService) { this.incidentService = incidentService; } - get serviceName() { - return IncidentController.SERVICE_NAME; - } - getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { try { - const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {}; + const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); const teamId = requireTeamId(req.user?.teamId); const result = await this.incidentService.getIncidentsByTeam( teamId, - sortOrder, - dateRange, - page, - rowsPerPage, - status, - monitorId, - resolutionType + validatedQuery.sortOrder, + validatedQuery.dateRange, + validatedQuery.page, + validatedQuery.rowsPerPage, + validatedQuery.status, + validatedQuery.monitorId, + validatedQuery.resolutionType ); return res.status(200).json({ @@ -45,10 +40,9 @@ class IncidentController { getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req.user?.teamId); + const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query); - const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; - - const summary = await this.incidentService.getIncidentSummary(teamId, limit); + const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); return res.status(200).json({ success: true, msg: "Incident summary retrieved successfully", diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 710336016..001bc776c 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -2,7 +2,6 @@ const SERVICE_NAME = "incidentService"; import type { Monitor } from "@/types/monitor.js"; import type { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { getDateForRange } from "@/utils/dataUtils.js"; import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; import type { Incident, IncidentSummary, User } from "@/types/index.js"; @@ -22,13 +21,13 @@ export interface IIncidentService { teamId: string, sortOrder: string, dateRange: string, - page: string, - rowsPerPage: string, - status: string, - monitorId: string, - resolutionType: string + page: number, + rowsPerPage: number, + status: boolean | undefined, + monitorId: string | undefined, + resolutionType: string | undefined ): Promise<{ incidents: Incident[]; count: number }>; - getIncidentSummary(teamId: string, limit?: string): Promise; + getIncidentSummary(teamId: string, limit?: number): Promise; getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>; } @@ -164,13 +163,13 @@ export class IncidentService implements IIncidentService { }); return resolvedIncident; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "resolveIncident", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { id: incidentId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -180,11 +179,11 @@ export class IncidentService implements IIncidentService { teamId: string, sortOrder: string, dateRange: string, - page: string, - rowsPerPage: string, - status: string, - monitorId: string, - resolutionType: string + page: number, + rowsPerPage: number, + status: boolean | undefined, + monitorId: string | undefined, + resolutionType: string | undefined ) => { try { if (!teamId) { @@ -193,9 +192,8 @@ export class IncidentService implements IIncidentService { const startDate = getDateForRange(dateRange); - const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; - const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; - const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status); + const parsedPage = page ?? 0; + const parsedRowsPerPage = rowsPerPage ?? 20; const incidents = await this.incidentsRepository.findByTeamId( teamId, @@ -203,7 +201,7 @@ export class IncidentService implements IIncidentService { parsedPage, parsedRowsPerPage, sortOrder, - parsedStatus, + status, monitorId, resolutionType ); @@ -211,35 +209,35 @@ export class IncidentService implements IIncidentService { const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType); return { incidents, count }; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "getIncidentsByTeam", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { teamId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } }; - getIncidentSummary = async (teamId: string, limit?: string) => { + getIncidentSummary = async (teamId: string, limit?: number) => { try { if (!teamId) { throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 }); } - const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10; + const parsedLimit = limit ?? 10; const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit); return summary; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "getIncidentSummary", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { teamId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -254,13 +252,13 @@ export class IncidentService implements IIncidentService { user = await this.usersRepository.findById(incident.resolvedBy); } return { incident, monitor, user }; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "getIncidentById", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { incidentId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } diff --git a/server/src/validation/incidentValidation.ts b/server/src/validation/incidentValidation.ts new file mode 100644 index 000000000..acbc857dc --- /dev/null +++ b/server/src/validation/incidentValidation.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import { booleanCoercion } from "./shared.js"; + +//**************************************** +// Incident Validations +//**************************************** + +export const getIncidentsByTeamQueryValidation = z.object({ + sortOrder: z.enum(["asc", "desc"]), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), + page: z.coerce.number().int().min(0), + rowsPerPage: z.coerce.number().int().min(1), + status: booleanCoercion.optional(), + monitorId: z.string().optional(), + resolutionType: z.enum(["manual", "automatic"]).optional(), +}); + +export const getIncidentSummaryQueryValidation = z.object({ + limit: z.coerce.number().int().min(1).optional(), +}); diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts index 7439f043d..cc1d33372 100644 --- a/server/src/validation/index.ts +++ b/server/src/validation/index.ts @@ -20,3 +20,4 @@ export * from "./settingsValidation.js"; export * from "./statusPageValidation.js"; export * from "./notificationValidation.js"; export * from "./userValidation.js"; +export * from "./incidentValidation.js"; From d00889ff9bb55980d73dca85bd4bb5bfa8a65285 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 11:14:44 -0700 Subject: [PATCH 14/30] renamed variable --- server/src/service/business/incidentService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 001bc776c..4790f9aac 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -206,7 +206,7 @@ export class IncidentService implements IIncidentService { resolutionType ); - const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType); + const count = await this.incidentsRepository.countByTeamId(teamId, startDate, status, monitorId, resolutionType); return { incidents, count }; } catch (error: unknown) { From 566011c4e9dd27026f4f4f47f06b4987080d5ec4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 11:35:36 -0700 Subject: [PATCH 15/30] fix inviteController typing --- server/src/controllers/controllerUtils.ts | 40 +++++++++------------- server/src/controllers/inviteController.ts | 14 ++++---- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index f8447fd59..17159a03c 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -3,7 +3,7 @@ import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index import sslChecker, { SSLDetails } from "ssl-checker"; type SSLCheckerType = typeof sslChecker; -const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise => { +export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; const cert = await checker(hostname); @@ -12,14 +12,14 @@ const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor } return cert; }; -const requireString = (value: unknown, fieldName: string): string => { +export const requireString = (value: unknown, fieldName: string): string => { if (typeof value === "string" && value.trim().length > 0) { return value; } throw new AppError({ message: `${fieldName} is required`, status: 400 }); }; -const optionalString = (value: unknown, fieldName: string): string | undefined => { +export const optionalString = (value: unknown, fieldName: string): string | undefined => { if (value === undefined) { return undefined; } @@ -29,7 +29,7 @@ const optionalString = (value: unknown, fieldName: string): string | undefined = throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); }; -const optionalNumber = (value: unknown, fieldName: string): number | undefined => { +export const optionalNumber = (value: unknown, fieldName: string): number | undefined => { if (value === undefined) { return undefined; } @@ -45,7 +45,7 @@ const optionalNumber = (value: unknown, fieldName: string): number | undefined = throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); }; -const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { +export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { if (value === undefined) { return undefined; } @@ -63,7 +63,7 @@ const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); }; -const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { +export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { const parseSingle = (input: unknown): MonitorType => { if (typeof input !== "string") { throw new AppError({ message: "Monitor type must be a string", status: 400 }); @@ -83,7 +83,7 @@ const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | u return parseSingle(value); }; -const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { +export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { if (value === undefined) { return undefined; } @@ -93,42 +93,36 @@ const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); }; -const requireTeamId = (teamId?: string): string => { +export const requireTeamId = (teamId?: string): string => { if (!teamId) { throw new AppError({ message: "Team ID is required", status: 400 }); } return teamId; }; -const requireUserId = (userId?: string): string => { +export const requireUserId = (userId?: string): string => { if (!userId) { throw new AppError({ message: "User ID is required", status: 400 }); } return userId; }; -const requireUserEmail = (userEmail?: string): string => { +export const requireUserEmail = (userEmail?: string): string => { if (!userEmail) { throw new AppError({ message: "User email is required", status: 400 }); } return userEmail; }; +export const requireFirstName = (firstName?: string): string => { + if (!firstName) { + throw new AppError({ message: "First name is required", status: 400 }); + } + return firstName; +}; + export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => { if (!userRoles || userRoles.length === 0) { throw new AppError({ message: "User roles are required", status: 400 }); } return userRoles; }; - -export { - fetchMonitorCertificate, - requireString, - optionalString, - optionalNumber, - optionalBoolean, - parseMonitorTypeFilter, - parseSortOrder, - requireTeamId, - requireUserId, - requireUserEmail, -}; diff --git a/server/src/controllers/inviteController.ts b/server/src/controllers/inviteController.ts index b17d32e98..84f6c30ae 100755 --- a/server/src/controllers/inviteController.ts +++ b/server/src/controllers/inviteController.ts @@ -1,12 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js"; -import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; +import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; +import { IInviteService } from "@/service/index.js"; const SERVICE_NAME = "inviteController"; class InviteController { static SERVICE_NAME = SERVICE_NAME; - private inviteService: any; - constructor(inviteService: any) { + private inviteService: IInviteService; + constructor(inviteService: IInviteService) { this.inviteService = inviteService; } @@ -34,8 +35,9 @@ class InviteController { sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - const userRoles = requireUserRoles(req?.user?.role); + const teamId = requireTeamId(req.user?.teamId); + const userRoles = requireUserRoles(req.user?.role); + const firstName = requireFirstName(req.user?.firstName); const inviteRequest = req.body; inviteRequest.teamId = teamId; @@ -43,7 +45,7 @@ class InviteController { const inviteToken = await this.inviteService.sendInviteEmail({ invite: inviteRequest, - firstName: req?.user?.firstName, + firstName, userRoles, }); return res.status(200).json({ From d07f25982967bc1aedadbaaf1b311c4845711459 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 11:37:17 -0700 Subject: [PATCH 16/30] properly type logController --- server/src/controllers/logController.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/controllers/logController.ts b/server/src/controllers/logController.ts index d99de1e9c..e26bc2e5e 100644 --- a/server/src/controllers/logController.ts +++ b/server/src/controllers/logController.ts @@ -1,11 +1,12 @@ +import { ILogger } from "@/utils/logger.js"; import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "LogController"; class LogController { static SERVICE_NAME = SERVICE_NAME; - private logger: any; - constructor(logger: any) { + private logger: ILogger; + constructor(logger: ILogger) { this.logger = logger; } get serviceName() { @@ -14,7 +15,7 @@ class LogController { getLogs = async (req: Request, res: Response, next: NextFunction) => { try { - const logs = await this.logger.getLogs(); + const logs = this.logger.getLogs(); res.status(200).json({ success: true, msg: "Logs fetched successfully", From e887c2308a9ff055b44d6986793ed6426a645605 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 11:50:51 -0700 Subject: [PATCH 17/30] type MWController --- .../maintenanceWindowController.ts | 40 +++++++++---------- .../IMaintenanceWindowsRepository.ts | 4 +- .../MongoMaintenanceWindowsRepository.ts | 8 ++-- .../business/maintenanceWindowService.ts | 30 ++++++++++---- .../validation/maintenanceWindowValidation.ts | 6 +-- 5 files changed, 52 insertions(+), 36 deletions(-) diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index ed16911bf..cd2174c1b 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -9,25 +9,18 @@ import { deleteMaintenanceWindowByIdParamValidation, } from "@/validation/maintenanceWindowValidation.js"; import { requireTeamId } from "@/controllers/controllerUtils.js"; - -const SERVICE_NAME = "maintenanceWindowController"; +import { IMaintenanceWindowService } from "@/service/index.js"; class MaintenanceWindowController { - static SERVICE_NAME = SERVICE_NAME; - private maintenanceWindowService: any; - constructor(maintenanceWindowService: any) { + private maintenanceWindowService: IMaintenanceWindowService; + constructor(maintenanceWindowService: IMaintenanceWindowService) { this.maintenanceWindowService = maintenanceWindowService; } - get serviceName() { - return MaintenanceWindowController.SERVICE_NAME; - } - createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { try { createMaintenanceWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); return res.status(200).json({ @@ -58,11 +51,18 @@ class MaintenanceWindowController { getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); + const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ + teamId, + active: validatedQuery.active, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + field: validatedQuery.field, + order: validatedQuery.order, + }); return res.status(200).json({ success: true, @@ -76,12 +76,12 @@ class MaintenanceWindowController { getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); + const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ - monitorId: req.params.monitorId as string, + monitorId: validatedParams.monitorId, teamId, }); @@ -96,11 +96,11 @@ class MaintenanceWindowController { }; deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { try { - deleteMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId }); + await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); return res.status(200).json({ success: true, @@ -113,14 +113,14 @@ class MaintenanceWindowController { editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { try { - editMaintenanceWindowByIdParamValidation.parse(req.params); - editMaintenanceByIdWindowBodyValidation.parse(req.body); + const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req.user?.teamId); const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ - id: req.params.id as string, - body: req.body, + id: validatedParams.id, + body: validatedBody, teamId, }); return res.status(200).json({ diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts index 1bcb02347..fca63ac35 100644 --- a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository { // fetch findById(id: string, teamId: string): Promise; findByMonitorId(monitorId: string, teamId: string): Promise; - findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise; + findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise; // update updateById(id: string, teamId: string, data: Partial): Promise; // delete deleteById(id: string, teamId: string): Promise; // other - countByTeamId(teamId: string, active: boolean): Promise; + countByTeamId(teamId: string, active?: boolean): Promise; } diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts index 4aadf9cc2..d924613ee 100644 --- a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -75,11 +75,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository findByTeamId = async ( teamId: string, - active: boolean, page: number, rowsPerPage: number, - field: string, - order: string + field?: string, + order?: string, + active?: boolean ): Promise => { const maintenanceQuery: Record = { teamId }; @@ -126,7 +126,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository } return this.toEntity(deleted); }; - countByTeamId = async (teamId: string, active: boolean) => { + countByTeamId = async (teamId: string, active?: boolean) => { const maintenanceQuery: Record = { teamId }; if (active !== undefined) maintenanceQuery.active = active; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index abf513eb5..1db2df9ca 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -10,7 +10,11 @@ export interface IMaintenanceWindowService { getMaintenanceWindowById(params: { id: string; teamId: string }): Promise; getMaintenanceWindowsByTeamId(params: { teamId: string; - query: Record; + active?: boolean; + page?: number; + rowsPerPage?: number; + field?: string; + order?: string; }): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>; getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise; deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise; @@ -72,14 +76,26 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.findById(id, teamId); }; - getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => { - let { active, page, rowsPerPage, field, order } = query || {}; - + getMaintenanceWindowsByTeamId = async ({ + teamId, + active, + page, + rowsPerPage, + field, + order, + }: { + teamId: string; + active?: boolean; + page?: number; + rowsPerPage?: number; + field?: string; + order?: string; + }) => { active = typeof active === "undefined" ? undefined : ParseBoolean(active); - page = parseInt(page) || 0; - rowsPerPage = parseInt(rowsPerPage) || 10; + page = page ?? 0; + rowsPerPage = rowsPerPage ?? 10; - const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order); + const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, page, rowsPerPage, field, order, active); const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active); return { maintenanceWindows, maintenanceWindowCount }; }; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index e09ac2989..3470f4c38 100644 --- a/server/src/validation/maintenanceWindowValidation.ts +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -45,9 +45,9 @@ 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(), + start: z.string().optional(), + end: z.string().optional(), + expiry: z.string().optional(), monitors: z.array(z.unknown()).optional(), duration: z.number().optional(), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), From 456237ba4e4c33d8b52b6ac56b28e17b7f4e3c8b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 12:00:19 -0700 Subject: [PATCH 18/30] tighten date typing --- .../src/controllers/maintenanceWindowController.ts | 4 ++-- .../service/business/maintenanceWindowService.ts | 2 -- .../src/validation/maintenanceWindowValidation.ts | 14 ++++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index cd2174c1b..d2338cd51 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -19,9 +19,9 @@ class MaintenanceWindowController { createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { try { - createMaintenanceWindowBodyValidation.parse(req.body); + const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); + await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody }); return res.status(200).json({ success: true, diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 1db2df9ca..98b803bd1 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,6 +1,5 @@ import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; import type { MaintenanceWindow } from "@/types/index.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "maintenanceWindowService"; @@ -91,7 +90,6 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { field?: string; order?: string; }) => { - active = typeof active === "undefined" ? undefined : ParseBoolean(active); page = page ?? 0; rowsPerPage = rowsPerPage ?? 10; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index 3470f4c38..7cac8d1ad 100644 --- a/server/src/validation/maintenanceWindowValidation.ts +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; +const dateToString = z.coerce.date().transform((d) => d.toISOString()); + //**************************************** // Maintenance Window Validations //**************************************** @@ -11,10 +13,10 @@ export const createMaintenanceWindowBodyValidation = z.object({ 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(), + start: dateToString, + end: dateToString, repeat: z.number().min(0, "Repeat must be a non-negative number"), - expiry: z.coerce.date().optional(), + expiry: dateToString.optional(), }); export const getMaintenanceWindowByIdParamValidation = z.object({ @@ -45,9 +47,9 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({ active: z.boolean().optional(), name: z.string().optional(), repeat: z.number().optional(), - start: z.string().optional(), - end: z.string().optional(), - expiry: z.string().optional(), + start: dateToString.optional(), + end: dateToString.optional(), + expiry: dateToString.optional(), monitors: z.array(z.unknown()).optional(), duration: z.number().optional(), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), From 42f6c42ae14437790340c4423c88b82c2c1ce7e9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 12:04:28 -0700 Subject: [PATCH 19/30] fix discarded validation, tighten types --- server/src/controllers/maintenanceWindowController.ts | 4 ++-- server/src/validation/maintenanceWindowValidation.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index d2338cd51..2e0294db1 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -33,11 +33,11 @@ class MaintenanceWindowController { }; getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { try { - getMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); const teamId = requireTeamId(req.user?.teamId); - const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId }); + const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); return res.status(200).json({ success: true, diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index 7cac8d1ad..bee06e7bc 100644 --- a/server/src/validation/maintenanceWindowValidation.ts +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -50,7 +50,7 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({ start: dateToString.optional(), end: dateToString.optional(), expiry: dateToString.optional(), - monitors: z.array(z.unknown()).optional(), + monitors: z.array(z.string()).optional(), duration: z.number().optional(), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), }); From 545926cfcf1bd0c829a3de4cd7c03eb3ec059689 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 12:41:27 -0700 Subject: [PATCH 20/30] fix validation in monitor controller --- server/src/controllers/monitorController.ts | 124 ++++++++---------- server/src/service/business/monitorService.ts | 6 +- server/src/types/monitor.ts | 3 +- server/src/validation/monitorValidation.ts | 77 +++++++++-- 4 files changed, 132 insertions(+), 78 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 1d222e6e2..dc78641c4 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -12,6 +12,10 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, + getUptimeDetailsByIdParamValidation, + getUptimeDetailsByIdQueryValidation, + importMonitorsBodyValidation, + sendTestEmailBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; import { @@ -45,10 +49,9 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { - getCertificateParamValidation.parse(req.params); + const validatedParams = getCertificateParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); - const monitorId = requireString(req.params?.monitorId, "Monitor ID"); - const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); return res.status(200).json({ @@ -65,9 +68,13 @@ class MonitorController { getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const normalize = optionalBoolean(req?.query?.normalize, "normalize"); + const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange; + const normalize = validatedQuery.normalize; + const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getUptimeDetailsById({ @@ -88,11 +95,11 @@ class MonitorController { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - getHardwareDetailsByIdParamValidation.parse(req.params); - getHardwareDetailsByIdQueryValidation.parse(req.query); + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent"; + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getHardwareDetailsById({ @@ -112,11 +119,11 @@ class MonitorController { }; getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - getHardwareDetailsByIdParamValidation.parse(req.params); - getHardwareDetailsByIdQueryValidation.parse(req.query); + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getPageSpeedDetailsById({ @@ -137,12 +144,12 @@ class MonitorController { getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - getMonitorByIdQueryValidation.parse(req.query); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const continentParam = req?.query?.continent; + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const continentParam = validatedQuery.continent; const continents = continentParam ? Array.isArray(continentParam) ? (continentParam as GeoContinent[]) @@ -169,11 +176,10 @@ class MonitorController { getMonitorById = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - getMonitorByIdQueryValidation.parse(req.query); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const monitorId = validatedParams.monitorId; const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -189,12 +195,12 @@ class MonitorController { createMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - createMonitorBodyValidation.parse(req.body); + const validatedBody = createMonitorBodyValidation.parse(req.body); const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); - const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); + const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); return res.status(200).json({ success: true, @@ -210,8 +216,8 @@ class MonitorController { try { const teamId = requireTeamId(req?.user?.teamId); const userId = requireUserId(req?.user?.id); - - const { monitors } = req.body; + const validatedBody = importMonitorsBodyValidation.parse(req.body); + const monitors = validatedBody.monitors; if (!monitors || !Array.isArray(monitors)) { throw new AppError({ message: "Invalid request: monitors array is required", status: 400 }); @@ -231,9 +237,9 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -250,7 +256,6 @@ class MonitorController { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req?.user?.teamId); - const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); return res.status(200).json({ @@ -264,12 +269,12 @@ class MonitorController { editMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - editMonitorBodyValidation.parse(req.body); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedBody = editMonitorBodyValidation.parse(req.body); + const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId); - const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); return res.status(200).json({ success: true, @@ -283,9 +288,9 @@ class MonitorController { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - pauseMonitorParamValidation.parse(req.params); + const validatedParams = pauseMonitorParamValidation.parse(req.params); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -302,7 +307,7 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const id = requireString(req?.user?.id, "User ID"); + const id = requireUserId(req?.user?.id); const teamId = requireTeamId(req?.user?.teamId); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); @@ -318,12 +323,8 @@ class MonitorController { sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { try { - const { to } = req.body; - if (!to || typeof to !== "string") { - throw new AppError({ message: "Invalid 'to' email address", status: 400 }); - } - - const messageId = await this.monitorService.sendTestEmail({ to }); + const validatedBody = sendTestEmailBodyValidation.parse(req.body); + const messageId = await this.monitorService.sendTestEmail({ to: validatedBody.to }); return res.status(200).json({ success: true, msg: "Test email sent successfully", @@ -336,12 +337,11 @@ class MonitorController { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorsByTeamIdParamValidation.parse(req.params); - getMonitorsByTeamIdQueryValidation.parse(req.query); + const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); - const type = parseMonitorTypeFilter(req.query?.type); - const filter = optionalString(req.query?.filter, "filter"); + const type = validatedQuery.type; + const filter = validatedQuery.filter; const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); @@ -357,16 +357,16 @@ class MonitorController { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorsByTeamIdParamValidation.parse(req.params); - getMonitorsWithChecksQueryValidation.parse(req.query); - const explain = optionalBoolean(req?.query?.explain, "explain"); - const limit = optionalNumber(req?.query?.limit, "limit"); - const page = optionalNumber(req?.query?.page, "page"); - const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage"); - const filter = optionalString(req?.query?.filter, "filter"); - const field = optionalString(req?.query?.field, "field"); - const order = parseSortOrder(req?.query?.order); - const type = parseMonitorTypeFilter(req?.query?.type); + const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); + + const explain = validatedQuery.explain; + const limit = validatedQuery.limit; + const page = validatedQuery.page; + const rowsPerPage = validatedQuery.rowsPerPage; + const filter = validatedQuery.filter; + const field = validatedQuery.field; + const order = validatedQuery.order; + const type = validatedQuery.type; const teamId = requireTeamId(req?.user?.teamId); const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ @@ -392,11 +392,7 @@ class MonitorController { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - + const teamId = requireTeamId(req?.user?.teamId); const json = await this.monitorService.exportMonitorsToJSON({ teamId }); return res.status(200).json({ @@ -424,11 +420,7 @@ class MonitorController { getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - + const teamId = requireTeamId(req.user?.teamId); const groups = await this.monitorService.getGroupsByTeamId({ teamId }); return res.status(200).json({ diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index a50598256..eea1b03d0 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -29,7 +29,7 @@ export interface IMonitorService { readonly serviceName: string; // create - createMonitor(teamId: string, userId: string, body: Monitor): Promise; + createMonitor(teamId: string, userId: string, body: Partial): Promise; createMonitors(monitors: Array, userId: string, teamId: string): Promise; addDemoMonitors(args: { userId: string; teamId: string }): Promise; @@ -64,7 +64,7 @@ export interface IMonitorService { getGroupsByTeamId(args: { teamId: string }): Promise; // update - editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; + editMonitor(args: { teamId: string; monitorId: string; body: Partial }): Promise; pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete @@ -430,7 +430,7 @@ export class MonitorService implements IMonitorService { return groups; }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial }) => { const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.jobQueue.updateJob(editedMonitor); return editedMonitor; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 89740162b..bac2313f5 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -12,7 +12,8 @@ export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupporte export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; -export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; +export const MonitorMatchMethods = ["equal", "include", "regex"] as const; +export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | ""; export interface Monitor { id: string; diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 75ffccd19..64d320653 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { GeoContinents } from "@/types/geoCheck.js"; +import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; export const getMonitorByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -25,14 +26,14 @@ export const getMonitorsByTeamIdQueryValidation = z.object({ z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])), ]) .optional(), - filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + filter: z.union([z.string(), z.literal("")]).optional(), }); export const getMonitorsWithChecksQueryValidation = z.object({ limit: z.coerce.number().int().min(1).max(100).optional(), page: z.coerce.number().int().min(0).optional(), rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), - filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + filter: z.union([z.string(), z.literal("")]).optional(), field: z.string().optional(), order: z.enum(["asc", "desc"]).optional(), type: z @@ -51,8 +52,8 @@ export const getCertificateParamValidation = z.object({ export const createMonitorBodyValidation = z.object({ _id: z.string().optional(), name: z.string().min(1, "Name is required"), - description: z.union([z.string(), z.null(), z.literal("")]).optional(), - type: z.string().min(1, "Type is required"), + description: z.union([z.string(), z.literal("")]).optional(), + type: z.enum(MonitorTypes, "Invalid monitor type"), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), url: z.string().min(1, "URL is required"), @@ -69,7 +70,7 @@ export const createMonitorBodyValidation = z.object({ secret: z.string().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), - matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(), grpcServiceName: z.union([z.string(), z.literal("")]).default(""), selectedDisks: z.array(z.string()).optional(), @@ -81,11 +82,11 @@ export const createMonitorBodyValidation = z.object({ export const editMonitorBodyValidation = z.object({ name: z.string().optional(), - type: z.string().optional(), + type: z.enum(MonitorTypes).optional(), url: z.string().optional(), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), - description: z.union([z.string(), z.null(), z.literal("")]).optional(), + description: z.union([z.string(), z.literal("")]).optional(), interval: z.number().optional(), notifications: z.array(z.string()).optional(), secret: z.string().optional(), @@ -93,7 +94,7 @@ export const editMonitorBodyValidation = z.object({ useAdvancedMatching: z.boolean().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), - matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), port: z.number().min(1).max(65535).optional(), cpuAlertThreshold: z.number().optional(), memoryAlertThreshold: z.number().optional(), @@ -108,10 +109,70 @@ export const editMonitorBodyValidation = z.object({ geoCheckInterval: z.number().min(300000).optional(), }); +export const sendTestEmailBodyValidation = z.object({ + to: z.string().email("Invalid 'to' email address"), +}); + export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); +export const getUptimeDetailsByIdParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getUptimeDetailsByIdQueryValidation = z.object({ + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), + normalize: booleanCoercion.optional(), +}); + +const importedMonitorSchema = z.object({ + id: z.string().optional(), + userId: z.string().optional(), + teamId: z.string().optional(), + name: z.string().min(1, "Name is required"), + description: z.union([z.string(), z.literal("")]).optional(), + status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).optional(), + statusWindow: z.array(z.boolean()).optional(), + statusWindowSize: z.number().optional(), + statusWindowThreshold: z.number().optional(), + type: z.enum(MonitorTypes, "Invalid monitor type"), + ignoreTlsErrors: z.boolean().optional(), + useAdvancedMatching: z.boolean().optional(), + jsonPath: z.union([z.string(), z.literal("")]).optional(), + expectedValue: z.union([z.string(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), + url: z.string().min(1, "URL is required"), + port: z.number().optional(), + isActive: z.boolean().optional(), + interval: z.number().optional(), + uptimePercentage: z.number().optional(), + notifications: z.array(z.string()).optional(), + secret: z.string().optional(), + cpuAlertThreshold: z.number().optional(), + cpuAlertCounter: z.number().optional(), + memoryAlertThreshold: z.number().optional(), + memoryAlertCounter: z.number().optional(), + diskAlertThreshold: z.number().optional(), + diskAlertCounter: z.number().optional(), + tempAlertThreshold: z.number().optional(), + tempAlertCounter: z.number().optional(), + selectedDisks: z.array(z.string()).optional(), + gameId: z.union([z.string(), z.literal("")]).optional(), + grpcServiceName: z.union([z.string(), z.literal("")]).optional(), + group: z.union([z.string(), z.null()]).optional(), + geoCheckEnabled: z.boolean().optional(), + geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), + geoCheckInterval: z.number().optional(), + recentChecks: z.array(z.unknown()).optional(), + createdAt: z.string().optional(), + updatedAt: z.string().optional(), +}); + +export const importMonitorsBodyValidation = z.object({ + monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"), +}); + export const getHardwareDetailsByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); From ff90953f17534920f86fbdcf72cbfffb1b28ffda Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 12:57:27 -0700 Subject: [PATCH 21/30] cleanup and tighten validation --- server/src/controllers/monitorController.ts | 57 ++++++++------------- server/src/validation/monitorValidation.ts | 2 +- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index dc78641c4..7310e7656 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -3,7 +3,6 @@ import { Request, Response, NextFunction } from "express"; import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, getMonitorsWithChecksQueryValidation, createMonitorBodyValidation, @@ -18,17 +17,7 @@ import { sendTestEmailBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { - fetchMonitorCertificate, - requireString, - optionalString, - optionalNumber, - optionalBoolean, - parseMonitorTypeFilter, - parseSortOrder, - requireTeamId, - requireUserId, -} from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireString, requireTeamId, requireUserId } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/service/index.js"; import { GeoContinent } from "@/types/geoCheck.js"; @@ -50,7 +39,7 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = getCertificateParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); @@ -75,7 +64,7 @@ class MonitorController { const dateRange = validatedQuery.dateRange; const normalize = validatedQuery.normalize; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getUptimeDetailsById({ teamId, @@ -100,7 +89,7 @@ class MonitorController { const monitorId = validatedParams.monitorId; const dateRange = validatedQuery.dateRange || "recent"; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getHardwareDetailsById({ teamId, @@ -124,7 +113,7 @@ class MonitorController { const monitorId = validatedParams.monitorId; const dateRange = validatedQuery.dateRange || "recent"; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getPageSpeedDetailsById({ teamId, @@ -150,12 +139,8 @@ class MonitorController { const monitorId = validatedParams.monitorId; const dateRange = validatedQuery.dateRange || "recent"; const continentParam = validatedQuery.continent; - const continents = continentParam - ? Array.isArray(continentParam) - ? (continentParam as GeoContinent[]) - : [continentParam as GeoContinent] - : undefined; - const teamId = requireTeamId(req?.user?.teamId); + const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getGeoChecksByMonitorId({ teamId, @@ -178,7 +163,7 @@ class MonitorController { try { const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitorId = validatedParams.monitorId; const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -197,8 +182,8 @@ class MonitorController { try { const validatedBody = createMonitorBodyValidation.parse(req.body); - const userId = requireString(req?.user?.id, "User ID"); - const teamId = requireTeamId(req?.user?.teamId); + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); @@ -214,8 +199,8 @@ class MonitorController { importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - const userId = requireUserId(req?.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); const validatedBody = importMonitorsBodyValidation.parse(req.body); const monitors = validatedBody.monitors; @@ -238,7 +223,7 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitorId = validatedParams.monitorId; const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -255,7 +240,7 @@ class MonitorController { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); return res.status(200).json({ @@ -272,7 +257,7 @@ class MonitorController { const validatedParams = getMonitorByIdParamValidation.parse(req.params); const validatedBody = editMonitorBodyValidation.parse(req.body); const monitorId = validatedParams.monitorId; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); @@ -291,7 +276,7 @@ class MonitorController { const validatedParams = pauseMonitorParamValidation.parse(req.params); const monitorId = validatedParams.monitorId; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -307,8 +292,8 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const id = requireUserId(req?.user?.id); - const teamId = requireTeamId(req?.user?.teamId); + const id = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); return res.status(200).json({ @@ -339,7 +324,7 @@ class MonitorController { try { const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const type = validatedQuery.type; const filter = validatedQuery.filter; @@ -367,7 +352,7 @@ class MonitorController { const field = validatedQuery.field; const order = validatedQuery.order; const type = validatedQuery.type; - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ teamId, @@ -392,7 +377,7 @@ class MonitorController { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); const json = await this.monitorService.exportMonitorsToJSON({ teamId }); return res.status(200).json({ diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 64d320653..1083dd5d1 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -14,7 +14,7 @@ export const getMonitorByIdQueryValidation = z.object({ dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), numToDisplay: z.coerce.number().optional(), normalize: booleanCoercion.optional(), - continent: z.enum(GeoContinents).optional(), + continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), }); export const getMonitorsByTeamIdParamValidation = z.object({}); From 20ff76df9db77f355638ca35cdb43f5b47d31a17 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:07:56 -0700 Subject: [PATCH 22/30] remove unsued route/validation --- server/src/controllers/monitorController.ts | 18 +----------------- server/src/routes/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 15 --------------- server/src/validation/monitorValidation.ts | 4 ---- 4 files changed, 1 insertion(+), 37 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 7310e7656..41d6d95bd 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -14,13 +14,11 @@ import { getUptimeDetailsByIdParamValidation, getUptimeDetailsByIdQueryValidation, importMonitorsBodyValidation, - sendTestEmailBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { fetchMonitorCertificate, requireString, requireTeamId, requireUserId } from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/service/index.js"; -import { GeoContinent } from "@/types/geoCheck.js"; const SERVICE_NAME = "monitorController"; class MonitorController { @@ -306,20 +304,6 @@ class MonitorController { } }; - sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = sendTestEmailBodyValidation.parse(req.body); - const messageId = await this.monitorService.sendTestEmail({ to: validatedBody.to }); - return res.status(200).json({ - success: true, - msg: "Test email sent successfully", - data: { messageId }, - }); - } catch (error) { - next(error); - } - }; - getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); diff --git a/server/src/routes/monitorRoute.ts b/server/src/routes/monitorRoute.ts index 476e421ce..7cd87ee1e 100755 --- a/server/src/routes/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -50,7 +50,6 @@ class MonitorRoutes { this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON); - this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); this.router.get("/games", this.monitorController.getAllGames); // Individual monitor CRUD routes diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index eea1b03d0..936044908 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -72,7 +72,6 @@ export interface IMonitorService { deleteAllMonitors(args: { teamId: string }): Promise; // other - sendTestEmail(args: { to: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>; } @@ -509,20 +508,6 @@ export class MonitorService implements IMonitorService { return deletedCount; }; - sendTestEmail = async ({ to }: { to: string }): Promise => { - const subject = "Test email from Checkmate"; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html); - - if (!messageId) { - throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 }); - } - - return messageId; - }; - exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 1083dd5d1..783f1a029 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -109,10 +109,6 @@ export const editMonitorBodyValidation = z.object({ geoCheckInterval: z.number().min(300000).optional(), }); -export const sendTestEmailBodyValidation = z.object({ - to: z.string().email("Invalid 'to' email address"), -}); - export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); From c156a49cc0cac662a2697e34bb7e8c033e219fb4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:10:09 -0700 Subject: [PATCH 23/30] remove dead check --- server/src/controllers/monitorController.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 41d6d95bd..e66af1306 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -202,10 +202,6 @@ class MonitorController { const validatedBody = importMonitorsBodyValidation.parse(req.body); const monitors = validatedBody.monitors; - if (!monitors || !Array.isArray(monitors)) { - throw new AppError({ message: "Invalid request: monitors array is required", status: 400 }); - } - const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); return res.status(200).json({ From 16b267d3a5ca28a3af4232f7c2422da1676f9b72 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:28:48 -0700 Subject: [PATCH 24/30] implement missing validation --- .../src/controllers/notificationController.ts | 81 +++++++------------ .../infrastructure/notificationsService.ts | 8 +- .../src/validation/notificationValidation.ts | 16 ++++ 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 0cee3726e..770bdcff4 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,16 +1,21 @@ import { Request, Response, NextFunction } from "express"; -import { Notification } from "@/types/index.js"; -import { createNotificationBodyValidation } from "@/validation/notificationValidation.js"; +import { + createNotificationBodyValidation, + deleteNotificationParamValidation, + getNotificationByIdParamValidation, + testNotificationBodyValidation, + editNotificationParamValidation, + testAllNotificationsBodyValidation, +} from "@/validation/notificationValidation.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; -import { requireTeamId } from "./controllerUtils.js"; +import { requireTeamId, requireUserId } from "./controllerUtils.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { - static SERVICE_NAME = SERVICE_NAME; private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { @@ -18,13 +23,9 @@ class NotificationController { this.monitorsRepository = monitorsRepository; } - get serviceName() { - return NotificationController.SERVICE_NAME; - } - testNotification = async (req: Request, res: Response, next: NextFunction) => { try { - const notification: Notification = req.body; + const notification = testNotificationBodyValidation.parse(req.body); const success = await this.notificationsService.sendTestNotification(notification); if (!success) { @@ -42,23 +43,12 @@ class NotificationController { createNotification = async (req: Request, res: Response, next: NextFunction) => { try { - createNotificationBodyValidation.parse(req.body); + const validatedBody = createNotificationBodyValidation.parse(req.body); - const body = req.body; + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - - const userId = req?.user?.id; - if (!userId) { - throw new AppError({ message: "User ID is required", status: 400 }); - } - body.userId = userId; - body.teamId = teamId; - - const notification = await this.notificationsService.createNotification(body); + const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); return res.status(200).json({ success: true, msg: "Notification created successfully", @@ -71,11 +61,7 @@ class NotificationController { getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - + const teamId = requireTeamId(req.user?.teamId); const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); return res.status(200).json({ @@ -90,17 +76,10 @@ class NotificationController { deleteNotification = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req.user?.teamId); + const validatedParams = deleteNotificationParamValidation.parse(req.params); - const notificationId = req.params.id as string; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } - - await this.notificationsService.deleteById(notificationId, teamId); + await this.notificationsService.deleteById(validatedParams.id, teamId); return res.status(200).json({ success: true, msg: "Notification deleted successfully", @@ -113,12 +92,9 @@ class NotificationController { getNotificationById = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req.user?.teamId); - const notificationId = req.params.id as string; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } + const validatedParams = getNotificationByIdParamValidation.parse(req.params); - const notification = await this.notificationsService.findById(notificationId, teamId); + const notification = await this.notificationsService.findById(validatedParams.id, teamId); return res.status(200).json({ success: true, @@ -132,14 +108,15 @@ class NotificationController { editNotification = async (req: Request, res: Response, next: NextFunction) => { try { - createNotificationBodyValidation.parse(req.body); + const validatedBody = createNotificationBodyValidation.parse(req.body); + const validatedParams = editNotificationParamValidation.parse(req.params); const teamId = requireTeamId(req.user?.teamId); - const notificationId = req.params.id as string; + const notificationId = validatedParams.id; if (!notificationId) { throw new AppError({ message: "Notification ID is required", status: 400 }); } - const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); return res.status(200).json({ success: true, msg: "Notification updated successfully", @@ -152,13 +129,11 @@ class NotificationController { testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { try { - const monitorId = req.body.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const validatedBody = testAllNotificationsBodyValidation.parse(req.body); - const monitor = await this.monitorsRepository.findById(monitorId, teamId); + const teamId = requireTeamId(req.user?.teamId); + + const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId); const notifications = monitor.notifications; if (notifications.length === 0) { diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index ea7b78a4f..ffd12e86a 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js"; import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js"; export interface INotificationsService { - createNotification: (notificationData: Partial) => Promise; + createNotification: (notificationData: Partial, userId: string, teamId: string) => Promise; findById: (id: string, teamId: string) => Promise; findNotificationsByTeamId: (teamId: string) => Promise; updateById(id: string, teamId: string, updateData: Partial): Promise; deleteById: (id: string, teamId: string) => Promise; handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise; - sendTestNotification: (notification: Notification) => Promise; + sendTestNotification: (notification: Partial) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; } @@ -174,7 +174,9 @@ export class NotificationsService implements INotificationsService { return true; }; - createNotification = async (notificationData: Partial): Promise => { + createNotification = async (notificationData: Partial, userId: string, teamId: string): Promise => { + notificationData.userId = userId; + notificationData.teamId = teamId; return await this.notificationsRepository.create(notificationData); }; diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index 5aa0b976e..9ae4e9fdc 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -67,6 +67,22 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ }), ]); +export const testNotificationBodyValidation = createNotificationBodyValidation; + +export const deleteNotificationParamValidation = z.object({ + id: z.string().min(1, "Notification ID is required"), +}); +export const getNotificationByIdParamValidation = z.object({ + id: z.string().min(1, "Notification ID is required"), +}); +export const editNotificationParamValidation = z.object({ + id: z.string().min(1, "Notification ID is required"), +}); + +export const testAllNotificationsBodyValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + export const sendTestEmailBodyValidation = z.object({ to: z.string().min(1, "To field is required"), systemEmailHost: z.string().optional(), From 94660903328cec032a0d129185d0616729f51bc8 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 11 Mar 2026 16:30:30 -0400 Subject: [PATCH 25/30] fix: rename "breached" to "threshold exceeded" in user-facing strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only changes user-facing text — no enum, type, or backend status changes. ## Changes - locales/en.json: "breached" → "threshold exceeded" - notificationMessageBuilder.ts: notification title/summary/details - email.ts: email subject line --- .../Components/design-elements/StatusBox.tsx | 4 +-- .../design-elements/StatusLabel.tsx | 4 +-- .../monitors/HeaderMonitorsSummary.tsx | 8 +++--- .../Pages/Infrastructure/Monitors/index.tsx | 2 +- client/src/Types/Monitor.ts | 4 +-- client/src/Utils/MonitorUtils.ts | 2 +- client/src/locales/en.json | 2 +- .../0005_renameBreachedToExceeded.ts | 25 ------------------- server/src/db/migration/index.ts | 2 -- .../monitors/MongoMonitorsRepository.ts | 6 ++--- .../SuperSimpleQueueHelper.ts | 6 ++--- .../service/infrastructure/statusService.ts | 22 ++++++++-------- server/src/types/monitor.ts | 4 +-- 13 files changed, 32 insertions(+), 59 deletions(-) delete mode 100644 server/src/db/migration/0005_renameBreachedToExceeded.ts diff --git a/client/src/Components/design-elements/StatusBox.tsx b/client/src/Components/design-elements/StatusBox.tsx index 895709b04..96a9d9fbf 100644 --- a/client/src/Components/design-elements/StatusBox.tsx +++ b/client/src/Components/design-elements/StatusBox.tsx @@ -139,12 +139,12 @@ export const InitializingStatusBox = ({ n }: { n: number }) => { /> ); }; -export const ExceededStatusBox = ({ n }: { n: number }) => { +export const BreachedStatusBox = ({ n }: { n: number }) => { const theme = useTheme(); const { t } = useTranslation(); return ( diff --git a/client/src/Components/design-elements/StatusLabel.tsx b/client/src/Components/design-elements/StatusLabel.tsx index e9c92a490..d50058378 100644 --- a/client/src/Components/design-elements/StatusLabel.tsx +++ b/client/src/Components/design-elements/StatusLabel.tsx @@ -21,8 +21,8 @@ export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProp return t("pages.common.monitors.status.up"); } else if (status === "down") { return t("pages.common.monitors.status.down"); - } else if (status === "exceeded") { - return t("pages.common.monitors.status.exceeded"); + } else if (status === "breached") { + return t("pages.common.monitors.status.breached"); } else if (status === "maintenance") { return t("pages.common.monitors.status.maintenance"); } else if (status === "paused") { diff --git a/client/src/Components/monitors/HeaderMonitorsSummary.tsx b/client/src/Components/monitors/HeaderMonitorsSummary.tsx index 07edc406a..1d130209d 100644 --- a/client/src/Components/monitors/HeaderMonitorsSummary.tsx +++ b/client/src/Components/monitors/HeaderMonitorsSummary.tsx @@ -3,7 +3,7 @@ import { DownStatusBox, PausedStatusBox, InitializingStatusBox, - ExceededStatusBox, + BreachedStatusBox, } from "@/Components/design-elements"; import Stack from "@mui/material/Stack"; @@ -12,12 +12,12 @@ import { useTheme } from "@mui/material"; interface MonitorsSummaryProps { summary: MonitorsSummary | null; - showExceeded?: boolean; + showBreached?: boolean; } export const HeaderMonitorsSummary = ({ summary, - showExceeded = false, + showBreached = false, }: MonitorsSummaryProps) => { const theme = useTheme(); return ( @@ -27,7 +27,7 @@ export const HeaderMonitorsSummary = ({ > - {showExceeded && } + {showBreached && } diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx index 53c3f16e0..bdf477613 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.tsx +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -135,7 +135,7 @@ const InfrastructureMonitors = () => { /> { if (status === "down") { return "error"; } - if (status === "exceeded") { + if (status === "breached") { return "error"; } return "warning"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 85357772e..04e18e3dc 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -401,7 +401,7 @@ }, "status": { "down": "down", - "exceeded": "threshold exceeded", + "breached": "threshold exceeded", "initializing": "initializing", "maintenance": "maintenance", "paused": "paused", diff --git a/server/src/db/migration/0005_renameBreachedToExceeded.ts b/server/src/db/migration/0005_renameBreachedToExceeded.ts deleted file mode 100644 index ea2f9d24e..000000000 --- a/server/src/db/migration/0005_renameBreachedToExceeded.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MonitorModel } from "../models/Monitor.js"; -import { logger } from "@/utils/logger.js"; - -/** - * Rename monitor status "breached" to "exceeded" for clarity. - * The word "breached" was confusing users as it implied a security breach. - */ -export async function renameBreachedToExceeded(): Promise { - const SERVICE_NAME = "Migration:RenameBreachedToExceeded"; - - try { - logger.info({ service: SERVICE_NAME, message: "Starting status rename: breached -> exceeded" }); - - const result = await MonitorModel.updateMany({ status: "breached" }, { $set: { status: "exceeded" } }); - - logger.info({ - service: SERVICE_NAME, - message: `Updated ${result.modifiedCount} monitors from 'breached' to 'exceeded'`, - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error({ service: SERVICE_NAME, message: `Error renaming status: ${errorMessage}` }); - throw error; - } -} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index 61bf358eb..3b7b45ada 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -2,7 +2,6 @@ import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshol import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js"; -import { renameBreachedToExceeded } from "./0005_renameBreachedToExceeded.js"; import MigrationModel from "../models/Migration.js"; type MigrationEntry = { @@ -15,7 +14,6 @@ const migrations: MigrationEntry[] = [ { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, - { name: "0005_renameBreachedToExceeded", execute: renameBreachedToExceeded }, ]; const runMigrations = async (logger?: { info: Function; error: Function }) => { diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a075d42b9..a3b40f459 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -262,9 +262,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { $cond: [{ $eq: ["$status", "maintenance"] }, 1, 0], }, }, - exceededMonitors: { + breachedMonitors: { $sum: { - $cond: [{ $eq: ["$status", "exceeded"] }, 1, 0], + $cond: [{ $eq: ["$status", "breached"] }, 1, 0], }, }, }, @@ -281,7 +281,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { pausedMonitors: 0, initializingMonitors: 0, maintenanceMonitors: 0, - exceededMonitors: 0, + breachedMonitors: 0, } ); }; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index e5d746635..b6908127b 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -440,14 +440,14 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { decision.shouldSendNotification = true; decision.incidentReason = "status_down"; decision.notificationReason = "status_change"; - } else if (monitor.status === "exceeded") { + } else if (monitor.status === "breached") { // Hardware monitor exceeded thresholds decision.shouldCreateIncident = true; decision.shouldSendNotification = true; decision.incidentReason = "threshold_breach"; decision.notificationReason = "threshold_breach"; - } else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "exceeded")) { - // Monitor recovered from down or exceeded state + } else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "breached")) { + // Monitor recovered from down or breached state decision.shouldResolveIncident = true; decision.shouldSendNotification = true; decision.notificationReason = "status_change"; diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index ef8c6125e..badcf78ec 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -292,7 +292,7 @@ export class StatusService implements IStatusService { temp: tempBreach, }; - // Update counters: decrement if exceeded, reset to 5 if not exceeded + // Update counters: decrement if breached, reset to 5 if not breached if (cpuBreach) { monitor.cpuAlertCounter = Math.max(0, monitor.cpuAlertCounter - 1); } else { @@ -321,23 +321,23 @@ export class StatusService implements IStatusService { const anyCounterZero = monitor.cpuAlertCounter === 0 || monitor.memoryAlertCounter === 0 || monitor.diskAlertCounter === 0 || monitor.tempAlertCounter === 0; - const anyThresholdExceeded = cpuBreach || memoryBreach || diskBreach || tempBreach; + const anyThresholdBreached = cpuBreach || memoryBreach || diskBreach || tempBreach; const allThresholdsNormal = !cpuBreach && !memoryBreach && !diskBreach && !tempBreach; - // Update monitor status based on threshold exceeded state + // Update monitor status based on threshold breach state if (newStatus !== "down") { // Don't override "down" status - service unreachable takes precedence // Check current monitor status, not newStatus for comparison - if (anyCounterZero && anyThresholdExceeded && monitor.status !== "exceeded") { - // Threshold exceeded: counter hit zero, change status to exceeded - newStatus = "exceeded"; + if (anyCounterZero && anyThresholdBreached && monitor.status !== "breached") { + // Initial breach: counter hit zero, change status to breached + newStatus = "breached"; statusChanged = true; - } else if (anyCounterZero && anyThresholdExceeded && monitor.status === "exceeded") { - // Already exceeded, keep status but don't mark as changed - newStatus = "exceeded"; + } else if (anyCounterZero && anyThresholdBreached && monitor.status === "breached") { + // Already breached, keep status but don't mark as changed + newStatus = "breached"; // statusChanged remains false - } else if (allThresholdsNormal && monitor.status === "exceeded") { - // All thresholds returned to normal, recover from exceeded state + } else if (allThresholdsNormal && monitor.status === "breached") { + // All thresholds returned to normal, recover from breached state newStatus = "up"; statusChanged = true; } diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 62a0d3aa6..89740162b 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -9,7 +9,7 @@ export type MonitorType = (typeof MonitorTypes)[number]; export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const; export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type); -export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "exceeded"] as const; +export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; @@ -64,7 +64,7 @@ export interface MonitorsSummary { pausedMonitors: number; initializingMonitors: number; maintenanceMonitors: number; - exceededMonitors: number; + breachedMonitors: number; } export interface MonitorsWithChecksByTeamIdResult { From e878f518879e678bc5c615db9e01edd0d648ad66 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:30:39 -0700 Subject: [PATCH 26/30] use service name --- server/src/controllers/notificationController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 770bdcff4..da396ba74 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -35,6 +35,7 @@ class NotificationController { return res.status(200).json({ success: true, msg: "Notification sent successfully", + details: { service: SERVICE_NAME }, }); } catch (error) { next(error); From 15d8a8ba1fd0dc695e298b2c32c539a0505fac01 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:40:35 -0700 Subject: [PATCH 27/30] fix test alert type --- .../notificationProviders/INotificationProvider.ts | 2 +- .../src/service/infrastructure/notificationProviders/discord.ts | 2 +- .../src/service/infrastructure/notificationProviders/email.ts | 2 +- .../src/service/infrastructure/notificationProviders/matrix.ts | 2 +- .../service/infrastructure/notificationProviders/pagerduty.ts | 2 +- .../src/service/infrastructure/notificationProviders/slack.ts | 2 +- .../src/service/infrastructure/notificationProviders/teams.ts | 2 +- .../src/service/infrastructure/notificationProviders/webhook.ts | 2 +- server/src/service/infrastructure/notificationsService.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 87056ef7a..2743dfab9 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -3,5 +3,5 @@ import type { NotificationMessage } from "@/types/notificationMessage.js"; export interface INotificationProvider { sendMessage: (notification: Notification, message: NotificationMessage) => Promise; - sendTestAlert(notification: Notification): Promise; + sendTestAlert(notification: Partial): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index e630e4e86..3bd2556ae 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -13,7 +13,7 @@ export class DiscordProvider implements INotificationProvider { this.logger = logger; } - sendTestAlert = async (notification: Notification) => { + sendTestAlert = async (notification: Partial) => { if (!notification.address) { return false; } diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index c14d59a85..7b61b1617 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -14,7 +14,7 @@ export class EmailProvider implements INotificationProvider { this.logger = logger; } - async sendTestAlert(notification: Notification): Promise { + async sendTestAlert(notification: Partial): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index ce53215bd..f14ea4d17 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -13,7 +13,7 @@ export class MatrixProvider implements INotificationProvider { this.logger = logger; } - sendTestAlert = async (notification: Notification) => { + sendTestAlert = async (notification: Partial) => { const { homeserverUrl, accessToken, roomId } = notification; if (!homeserverUrl || !accessToken || !roomId) { return false; diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index b15717878..304c38cf4 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -14,7 +14,7 @@ export class PagerDutyProvider implements INotificationProvider { this.logger = logger; } - async sendTestAlert(notification: Notification): Promise { + async sendTestAlert(notification: Partial): Promise { try { await got.post("https://events.pagerduty.com/v2/enqueue", { json: { diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index e13362669..cfb135901 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -13,7 +13,7 @@ export class SlackProvider implements INotificationProvider { this.logger = logger; } - async sendTestAlert(notification: Notification): Promise { + async sendTestAlert(notification: Partial): Promise { if (!notification.address) { return false; } diff --git a/server/src/service/infrastructure/notificationProviders/teams.ts b/server/src/service/infrastructure/notificationProviders/teams.ts index 2a4476807..dcf48c4cd 100644 --- a/server/src/service/infrastructure/notificationProviders/teams.ts +++ b/server/src/service/infrastructure/notificationProviders/teams.ts @@ -65,7 +65,7 @@ export class TeamsProvider implements INotificationProvider { this.logger = logger; } - async sendTestAlert(notification: Notification): Promise { + async sendTestAlert(notification: Partial): Promise { if (!notification.address) { return false; } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 6a00f3f1e..fcb5728d4 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -98,7 +98,7 @@ export class WebhookProvider implements INotificationProvider { }; } - sendTestAlert = async (notification: Notification) => { + sendTestAlert = async (notification: Partial) => { if (!notification.address) { return false; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index ffd12e86a..c75477c88 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -141,7 +141,7 @@ export class NotificationsService implements INotificationsService { return await this.sendNotifications(monitor, monitorStatusResponse, decision); }; - sendTestNotification = async (notification: Notification) => { + sendTestNotification = async (notification: Partial) => { switch (notification.type) { case "email": return await this.emailProvider.sendTestAlert(notification); From 76f550843b622245895f170c7c6467ea4b33213d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:45:17 -0700 Subject: [PATCH 28/30] remove dead check --- server/src/controllers/notificationController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index da396ba74..9c39daee7 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -114,9 +114,7 @@ class NotificationController { const teamId = requireTeamId(req.user?.teamId); const notificationId = validatedParams.id; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); return res.status(200).json({ success: true, From 61cc0c2befe9cf3fa74545f486760562deb94958 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 13:51:37 -0700 Subject: [PATCH 29/30] remove unused route --- server/src/controllers/queueController.ts | 18 +++--------------- server/src/routes/queueRoute.ts | 1 - .../SuperSimpleQueue/SuperSimpleQueue.ts | 2 +- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/server/src/controllers/queueController.ts b/server/src/controllers/queueController.ts index 9885bab87..5076e0b60 100755 --- a/server/src/controllers/queueController.ts +++ b/server/src/controllers/queueController.ts @@ -1,11 +1,12 @@ +import { ISuperSimpleQueue } from "@/service/index.js"; import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "JobQueueController"; class JobQueueController { static SERVICE_NAME = SERVICE_NAME; - private jobQueue: any; - constructor(jobQueue: any) { + private jobQueue: ISuperSimpleQueue; + constructor(jobQueue: ISuperSimpleQueue) { this.jobQueue = jobQueue; } @@ -77,18 +78,5 @@ class JobQueueController { next(error); } }; - - checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => { - try { - const stuckQueues = await this.jobQueue.checkQueueHealth(); - return res.status(200).json({ - success: true, - msg: "Queue health checked successfully", - data: stuckQueues, - }); - } catch (error) { - next(error); - } - }; } export default JobQueueController; diff --git a/server/src/routes/queueRoute.ts b/server/src/routes/queueRoute.ts index d5b036b15..80346d48c 100755 --- a/server/src/routes/queueRoute.ts +++ b/server/src/routes/queueRoute.ts @@ -14,7 +14,6 @@ class QueueRoutes { this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob); this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); - this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth); this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index f4fd5b1e6..151c41e5c 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -42,7 +42,7 @@ type QueueJobSummary = { export interface ISuperSimpleQueue { readonly serviceName: string; init(): Promise; - addJob(monitorId: string, monitor: Monitor): Promise; + addJob(monitorId: string, monitor?: Monitor): Promise; deleteJob(monitor: Monitor): Promise; pauseJob(monitor: Monitor): Promise; resumeJob(monitor: Monitor): Promise; From 1af609a5693b3c5b467839b9b3e27d3ba09de17b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 11 Mar 2026 14:08:20 -0700 Subject: [PATCH 30/30] remove unsued route/methods --- server/src/controllers/queueController.ts | 12 ------------ server/src/routes/queueRoute.ts | 1 - .../SuperSimpleQueue/SuperSimpleQueue.ts | 2 +- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/server/src/controllers/queueController.ts b/server/src/controllers/queueController.ts index 5076e0b60..9ae2b494a 100755 --- a/server/src/controllers/queueController.ts +++ b/server/src/controllers/queueController.ts @@ -54,18 +54,6 @@ class JobQueueController { } }; - addJob = async (req: Request, res: Response, next: NextFunction) => { - try { - await this.jobQueue.addJob(Math.random().toString(36).substring(7)); - return res.status(200).json({ - success: true, - msg: "Job added to queue successfully", - }); - } catch (error) { - next(error); - } - }; - flushQueue = async (req: Request, res: Response, next: NextFunction) => { try { const result = await this.jobQueue.flushQueues(); diff --git a/server/src/routes/queueRoute.ts b/server/src/routes/queueRoute.ts index 80346d48c..dd3b27999 100755 --- a/server/src/routes/queueRoute.ts +++ b/server/src/routes/queueRoute.ts @@ -11,7 +11,6 @@ class QueueRoutes { } initRoutes() { this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); - this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob); this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 151c41e5c..f4fd5b1e6 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -42,7 +42,7 @@ type QueueJobSummary = { export interface ISuperSimpleQueue { readonly serviceName: string; init(): Promise; - addJob(monitorId: string, monitor?: Monitor): Promise; + addJob(monitorId: string, monitor: Monitor): Promise; deleteJob(monitor: Monitor): Promise; pauseJob(monitor: Monitor): Promise; resumeJob(monitor: Monitor): Promise;