Merge branch 'upstream/develop' into feat/bulk-edit-notifications and resolve conflicts

This commit is contained in:
ashu130698 committed 2026-03-12 13:17:40 +05:30
commit 88a39fe502
41 files changed
+509 -494

No files matched your search

+7 -14
View File
@@ -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).
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
@@ -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.
+1 -1
View File
@@ -401,7 +401,7 @@
},
"status": {
"down": "down",
"breached": "breached",
"breached": "threshold exceeded",
"initializing": "initializing",
"maintenance": "maintenance",
"paused": "paused",
+1 -1
View File
@@ -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 .",
+1 -1
View File
@@ -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),
};
+44 -47
View File
@@ -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);
+22 -27
View File
@@ -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<any> => {
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
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,
};
@@ -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;
}
+15 -21
View File
@@ -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",
+8 -6
View File
@@ -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({
+4 -3
View File
@@ -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",
@@ -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({
+74 -116
View File
@@ -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({
@@ -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 });
+3 -27
View File
@@ -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;
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
// fetch
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>;
// update
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
// delete
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
// other
countByTeamId(teamId: string, active: boolean): Promise<number>;
countByTeamId(teamId: string, active?: boolean): Promise<number>;
}
@@ -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<MaintenanceWindow[]> => {
const maintenanceQuery: Record<string, any> = { 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<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
-1
View File
@@ -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
-2
View File
@@ -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);
}
+29 -31
View File
@@ -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<IncidentSummary>;
getIncidentSummary(teamId: string, limit?: number): Promise<IncidentSummary>;
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;
}
@@ -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<MaintenanceWindow>;
getMaintenanceWindowsByTeamId(params: {
teamId: string;
query: Record<string, string>;
active?: boolean;
page?: number;
rowsPerPage?: number;
field?: string;
order?: string;
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
@@ -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 };
};
+3 -18
View File
@@ -29,7 +29,7 @@ export interface IMonitorService {
readonly serviceName: string;
// create
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
@@ -64,7 +64,7 @@ export interface IMonitorService {
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
// update
editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
editMonitor(args: { teamId: string; monitorId: string; body: Partial<Monitor> }): Promise<Monitor>;
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
// delete
@@ -75,7 +75,6 @@ export interface IMonitorService {
updateNotifications(args: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise<number>;
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
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<Monitor> }) => {
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<string> => {
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<any[]> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
+46 -42
View File
@@ -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<User>, appSettings: { jwtTTL?: string; jwtSecret?: string }): string;
registerUser(user: Partial<User>, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>;
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: unknown): Promise<User>;
issueToken(payload: Partial<User>, appSettings: EnvConfig): string;
registerUser(user: Partial<User>, inviteToken: string, file: Express.Multer.File | null): Promise<{ user: User; token: string }>;
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise<User>;
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
editUser(updates: Partial<User & { newPassword?: string }>, file: unknown, currentUser: User): Promise<User>;
editUser(
updates: Partial<User & { newPassword?: string }>,
file: Express.Multer.File | null,
currentUserId: string,
currentUserEmail: string
): Promise<User>;
checkSuperadminExists(): Promise<boolean>;
requestRecovery(email: string): Promise<string | false | undefined>;
validateRecovery(recoveryToken: string): Promise<void>;
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
deleteUser(user: User): Promise<void>;
deleteUserById(actor: User, targetUserId: string): Promise<void>;
deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise<void>;
deleteUserById(params: { actorId: string; actorTeamId: string; actorRoles: UserRole[]; targetUserId: string }): Promise<void>;
getAllUsers(): Promise<User[]>;
getUserById(roles: UserRole[], userId: string): Promise<User>;
editUserById(userId: string, patch: Partial<User>): Promise<void>;
@@ -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<User>, 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<User>, inviteToken: string, file: any) => {
registerUser = async (user: Partial<User>, 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<User>, teamId: string, actorRoles: UserRole[], file: any) => {
createUser = async (userData: Partial<User>, 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<User & { newPassword?: string }>, file: any, currentUser: any) => {
editUser = async (
updates: Partial<User & { newPassword?: string }>,
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<User>) => {
editUserById = async (userId: string, patch: Partial<User>) => {
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;
@@ -15,7 +15,7 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
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: {
@@ -145,9 +145,9 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}
private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): 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);
@@ -3,5 +3,5 @@ import type { NotificationMessage } from "@/types/notificationMessage.js";
export interface INotificationProvider {
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
sendTestAlert(notification: Notification): Promise<boolean>;
sendTestAlert(notification: Partial<Notification>): Promise<boolean>;
}
@@ -13,7 +13,7 @@ export class DiscordProvider implements INotificationProvider {
this.logger = logger;
}
sendTestAlert = async (notification: Notification) => {
sendTestAlert = async (notification: Partial<Notification>) => {
if (!notification.address) {
return false;
}
@@ -14,7 +14,7 @@ export class EmailProvider implements INotificationProvider {
this.logger = logger;
}
async sendTestAlert(notification: Notification): Promise<boolean> {
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
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:
@@ -13,7 +13,7 @@ export class MatrixProvider implements INotificationProvider {
this.logger = logger;
}
sendTestAlert = async (notification: Notification) => {
sendTestAlert = async (notification: Partial<Notification>) => {
const { homeserverUrl, accessToken, roomId } = notification;
if (!homeserverUrl || !accessToken || !roomId) {
return false;
@@ -14,7 +14,7 @@ export class PagerDutyProvider implements INotificationProvider {
this.logger = logger;
}
async sendTestAlert(notification: Notification): Promise<boolean> {
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
try {
await got.post("https://events.pagerduty.com/v2/enqueue", {
json: {
@@ -13,7 +13,7 @@ export class SlackProvider implements INotificationProvider {
this.logger = logger;
}
async sendTestAlert(notification: Notification): Promise<boolean> {
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
if (!notification.address) {
return false;
}
@@ -65,7 +65,7 @@ export class TeamsProvider implements INotificationProvider {
this.logger = logger;
}
async sendTestAlert(notification: Notification): Promise<boolean> {
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
if (!notification.address) {
return false;
}
@@ -98,7 +98,7 @@ export class WebhookProvider implements INotificationProvider {
};
}
sendTestAlert = async (notification: Notification) => {
sendTestAlert = async (notification: Partial<Notification>) => {
if (!notification.address) {
return false;
}
@@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js";
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
export interface INotificationsService {
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>;
findById: (id: string, teamId: string) => Promise<Notification>;
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
deleteById: (id: string, teamId: string) => Promise<Notification>;
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
sendTestNotification: (notification: Notification) => Promise<boolean>;
sendTestNotification: (notification: Partial<Notification>) => Promise<boolean>;
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
}
@@ -141,7 +141,7 @@ export class NotificationsService implements INotificationsService {
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
};
sendTestNotification = async (notification: Notification) => {
sendTestNotification = async (notification: Partial<Notification>) => {
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<Notification>): Promise<Notification> => {
createNotification = async (notificationData: Partial<Notification>, userId: string, teamId: string): Promise<Notification> => {
notificationData.userId = userId;
notificationData.teamId = teamId;
return await this.notificationsRepository.create(notificationData);
};
+3 -3
View File
@@ -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,
+2 -1
View File
@@ -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;
+24
View File
@@ -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<SSLDetails>;
// Change 'export default' to 'export =' to allow it to be callable
export = sslChecker;
}
@@ -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(),
});
+1
View File
@@ -20,3 +20,4 @@ export * from "./settingsValidation.js";
export * from "./statusPageValidation.js";
export * from "./notificationValidation.js";
export * from "./userValidation.js";
export * from "./incidentValidation.js";
@@ -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(),
});
+66 -9
View File
@@ -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"),
});
@@ -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(),