diff --git a/README.md b/README.md index 07c0e84bd..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). @@ -175,9 +174,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. diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 4c4965aec..04e18e3dc 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -401,7 +401,7 @@ }, "status": { "down": "down", - "breached": "breached", + "breached": "threshold exceeded", "initializing": "initializing", "maintenance": "maintenance", "paused": "paused", 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 .", diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index f033a94da..00769f545 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -40,7 +40,7 @@ export const initializeControllers = (services: InitializedServices): Initialize queueController: new QueueController(services.jobQueue), logController: new LogController(services.logger), statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService), - notificationController: new NotificationController(services.notificationsService), + notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), diagnosticController: new DiagnosticController(services.diagnosticService), incidentController: new IncidentController(services.incidentService), }; diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 3f837f632..6eb49bbdd 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 validatedBody = registrationBodyValidation.parse(newUser); - const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); + const { user, token } = await this.userService.registerUser(validatedBody, 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", @@ -209,19 +223,9 @@ 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 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) { @@ -231,24 +235,19 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; + const actorRoles = requireUserRoles(req.user?.role); + const actorId = requireUserId(req.user?.id); - if (!roles || !roles.includes("superadmin")) { + if (!actorRoles.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 +256,14 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; - if (!roles || !roles.includes("superadmin")) { + const actorRoles = requireUserRoles(req.user?.role); + if (!actorRoles.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..17159a03c 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,24 +1,25 @@ import { AppError } from "@/utils/AppError.js"; -import { type MonitorType, MonitorTypes } from "@/types/index.js"; +import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; +import sslChecker, { SSLDetails } from "ssl-checker"; +type SSLCheckerType = typeof sslChecker; -const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { +export const fetchMonitorCertificate = async (checker: SSLCheckerType, 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"); } 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; } @@ -28,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; } @@ -44,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; } @@ -62,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 }); @@ -82,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; } @@ -92,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 requireUserRoles = (userRoles?: string[]): string[] => { +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/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; } 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/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({ 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", diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index ed16911bf..2e0294db1 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -9,26 +9,19 @@ 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 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, @@ -40,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, @@ -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/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 72821331c..a1d5fb242 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -3,7 +3,6 @@ import { updateNotificationsValidation } from "@/validation/notificationValidati import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, getMonitorsWithChecksQueryValidation, createMonitorBodyValidation, @@ -12,19 +11,12 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, + getUptimeDetailsByIdParamValidation, + getUptimeDetailsByIdQueryValidation, + importMonitorsBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { - fetchMonitorCertificate, - requireString, - optionalString, - optionalNumber, - optionalBoolean, - parseMonitorTypeFilter, - parseSortOrder, - requireTeamId, - requireUserId, -} from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorService, INotificationsService } from "@/service/index.js"; import { GeoContinent } from "@/types/geoCheck.js"; @@ -47,10 +39,9 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { - 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 validatedParams = getCertificateParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); return res.status(200).json({ @@ -67,10 +58,14 @@ 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 teamId = requireTeamId(req?.user?.teamId); + 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({ teamId, @@ -90,12 +85,12 @@ 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 teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getHardwareDetailsById({ teamId, @@ -114,12 +109,12 @@ 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 teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getPageSpeedDetailsById({ teamId, @@ -139,18 +134,14 @@ 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 continents = continentParam - ? Array.isArray(continentParam) - ? (continentParam as GeoContinent[]) - : [continentParam as GeoContinent] - : undefined; - const teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const continentParam = validatedQuery.continent; + const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getGeoChecksByMonitorId({ teamId, @@ -171,11 +162,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 teamId = requireTeamId(req.user?.teamId); + const monitorId = validatedParams.monitorId; const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -191,12 +181,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 userId = requireUserId(req.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,14 +200,10 @@ class MonitorController { importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - const userId = requireUserId(req?.user?.id); - - const { monitors } = req.body; - - if (!monitors || !Array.isArray(monitors)) { - throw new AppError({ message: "Invalid request: monitors array is required", status: 400 }); - } + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); + const validatedBody = importMonitorsBodyValidation.parse(req.body); + const monitors = validatedBody.monitors; const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); @@ -233,9 +219,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 teamId = requireTeamId(req?.user?.teamId); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); + const monitorId = validatedParams.monitorId; const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -251,8 +237,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({ @@ -266,12 +251,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 teamId = requireTeamId(req?.user?.teamId); + 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, @@ -285,10 +270,10 @@ 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 teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -304,8 +289,8 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const id = requireString(req?.user?.id, "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({ @@ -318,32 +303,13 @@ 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 }); - 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 { - 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 teamId = requireTeamId(req.user?.teamId); + const type = validatedQuery.type; + const filter = validatedQuery.filter; const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); @@ -359,17 +325,17 @@ 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 teamId = requireTeamId(req?.user?.teamId); + 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({ teamId, @@ -394,11 +360,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({ @@ -426,11 +388,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/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 7ea8c8f95..f6b53f8d3 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,27 +1,31 @@ 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 { INotificationsService } from "@/service/index.js"; -import { requireTeamId } from "./controllerUtils.js"; +import { requireTeamId, requireUserId } from "./controllerUtils.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { - static SERVICE_NAME = SERVICE_NAME; private notificationsService: INotificationsService; - constructor(notificationsService: INotificationsService) { + private monitorsRepository: IMonitorsRepository; + constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { this.notificationsService = notificationsService; - } - - get serviceName() { - return NotificationController.SERVICE_NAME; + this.monitorsRepository = monitorsRepository; } 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) { @@ -31,6 +35,7 @@ class NotificationController { return res.status(200).json({ success: true, msg: "Notification sent successfully", + details: { service: SERVICE_NAME }, }); } catch (error) { next(error); @@ -39,23 +44,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", @@ -68,11 +62,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({ @@ -87,17 +77,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", @@ -110,12 +93,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, @@ -129,14 +109,13 @@ 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; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } - const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); + const notificationId = validatedParams.id; + + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); return res.status(200).json({ success: true, msg: "Notification updated successfully", @@ -150,14 +129,12 @@ 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); - // Removed logic since that function shouldn't be here, testing notifications is separate from this controller. - const notifications: string[] = []; // Fix TS definition error temporarily. + const teamId = requireTeamId(req.user?.teamId); + + const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId); + const notifications = monitor.notifications || []; if (notifications.length === 0) { throw new AppError({ message: "No notifications", status: 400 }); diff --git a/server/src/controllers/queueController.ts b/server/src/controllers/queueController.ts index 9885bab87..9ae2b494a 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; } @@ -53,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(); @@ -77,18 +66,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/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/routes/monitorRoute.ts b/server/src/routes/monitorRoute.ts index 5b566d300..19b8e0764 100755 --- a/server/src/routes/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -51,7 +51,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/routes/queueRoute.ts b/server/src/routes/queueRoute.ts index d5b036b15..dd3b27999 100755 --- a/server/src/routes/queueRoute.ts +++ b/server/src/routes/queueRoute.ts @@ -11,10 +11,8 @@ 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("/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/business/incidentService.ts b/server/src/service/business/incidentService.ts index 710336016..4790f9aac 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,43 +201,43 @@ export class IncidentService implements IIncidentService { parsedPage, parsedRowsPerPage, sortOrder, - parsedStatus, + status, monitorId, 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: 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/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index abf513eb5..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"; @@ -10,7 +9,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 +75,25 @@ 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; + }) => { + page = page ?? 0; + rowsPerPage = rowsPerPage ?? 10; - active = typeof active === "undefined" ? undefined : ParseBoolean(active); - page = parseInt(page) || 0; - rowsPerPage = parseInt(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/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index fc52af6bd..9561b7d32 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 @@ -75,7 +75,6 @@ export interface IMonitorService { updateNotifications(args: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): 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[] }>; } @@ -433,7 +432,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; @@ -524,20 +523,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/service/business/userService.ts b/server/src/service/business/userService.ts index 51764667e..a4ef15cee 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/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: { 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/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..b3686651c 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); @@ -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/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 ea7b78a4f..c75477c88 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; } @@ -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); @@ -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/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, 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/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; +} 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"; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index e09ac2989..bee06e7bc 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,10 +47,10 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({ active: z.boolean().optional(), name: z.string().optional(), repeat: z.number().optional(), - start: z.coerce.date().optional(), - end: z.coerce.date().optional(), - expiry: z.coerce.date().optional(), - monitors: z.array(z.unknown()).optional(), + start: dateToString.optional(), + end: dateToString.optional(), + expiry: dateToString.optional(), + monitors: z.array(z.string()).optional(), duration: z.number().optional(), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), }); diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 75ffccd19..783f1a029 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"), @@ -13,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({}); @@ -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(), @@ -112,6 +113,62 @@ 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"), }); diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index cf7ea9ef5..cf187f7b4 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(),