-> timeseries

This commit is contained in:
Alex Holliday committed 2026-01-12 19:32:23 +00:00
1 parent 5c14c68899
commit e090d3cfd3
91 files changed
+941 -987

No files matched your search

+97
View File
@@ -57,6 +57,7 @@
"esm": "3.2.25",
"globals": "^15.14.0",
"mocha": "11.1.0",
"nodemon": "^3.1.11",
"prettier": "^3.3.3",
"sinon": "19.0.2",
"tsc-alias": "1.8.16",
@@ -6732,6 +6733,13 @@
"node": ">= 4"
}
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true,
"license": "ISC"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -8745,6 +8753,58 @@
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
"integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/nodemon/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/nodemon/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -9797,6 +9857,13 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -10424,6 +10491,19 @@
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sinon": {
"version": "19.0.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz",
@@ -11046,6 +11126,16 @@
"node": ">=0.6"
}
},
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"license": "ISC",
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
@@ -11225,6 +11315,13 @@
"node": ">=0.8.0"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true,
"license": "MIT"
},
"node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
+1
View File
@@ -69,6 +69,7 @@
"esm": "3.2.25",
"globals": "^15.14.0",
"mocha": "11.1.0",
"nodemon": "^3.1.11",
"prettier": "^3.3.3",
"sinon": "19.0.2",
"tsc-alias": "1.8.16",
-1
View File
@@ -14,7 +14,6 @@ import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js";
export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => {
const allowedOrigin = envSettings.clientHost;
const app = express();
app.use(generalApiLimiter);
// Static files
-47
View File
@@ -1,47 +0,0 @@
import { createCommonDependencies } from "../controllers/v1/baseController.js";
// Services
// V1 Controllers
import MonitorController from "../controllers/v1/monitorController.js";
import AuthController from "../controllers/v1/authController.js";
import SettingsController from "../controllers/v1/settingsController.js";
import CheckController from "../controllers/v1/checkController.js";
import InviteController from "../controllers/v1/inviteController.js";
import MaintenanceWindowController from "../controllers/v1/maintenanceWindowController.js";
import QueueController from "../controllers/v1/queueController.js";
import LogController from "../controllers/v1/logController.js";
import StatusPageController from "../controllers/v1/statusPageController.js";
import NotificationController from "../controllers/v1/notificationController.js";
import DiagnosticController from "../controllers/v1/diagnosticController.js";
import IncidentController from "../controllers/v1/incidentController.js";
export const initializeControllers = (services) => {
const controllers = {};
const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService);
// V1
controllers.authController = new AuthController(commonDependencies, {
settingsService: services.settingsService,
emailService: services.emailService,
jobQueue: services.jobQueue,
userService: services.userService,
});
controllers.monitorController = new MonitorController(services.monitorService);
controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db);
controllers.checkController = new CheckController(services.checkService);
controllers.inviteController = new InviteController(services.inviteService);
controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService);
controllers.queueController = new QueueController(services.jobQueue);
controllers.logController = new LogController(services.logger);
controllers.statusPageController = new StatusPageController(services.db);
controllers.notificationController = new NotificationController(services.notificationService, services.db);
controllers.diagnosticController = new DiagnosticController(services.diagnosticService);
controllers.incidentController = new IncidentController(services.incidentService);
return controllers;
};
+35
View File
@@ -0,0 +1,35 @@
import MonitorController from "../controllers/monitorController.js";
import AuthController from "../controllers/authController.js";
import SettingsController from "../controllers/settingsController.js";
import CheckController from "../controllers/checkController.js";
import InviteController from "../controllers/inviteController.js";
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
import QueueController from "../controllers/queueController.js";
import LogController from "../controllers/logController.js";
import StatusPageController from "../controllers/statusPageController.js";
import NotificationController from "../controllers/notificationController.js";
import DiagnosticController from "../controllers/diagnosticController.js";
import IncidentController from "../controllers/incidentController.js";
export const initializeControllers = (services: any) => {
const controllers: Record<string, any> = {};
controllers.authController = new AuthController(services.userService);
controllers.monitorController = new MonitorController(services.monitorService);
controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db);
controllers.checkController = new CheckController(services.checkService);
controllers.inviteController = new InviteController(services.inviteService);
controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService);
controllers.queueController = new QueueController(services.jobQueue);
controllers.logController = new LogController(services.logger);
controllers.statusPageController = new StatusPageController(services.db);
controllers.notificationController = new NotificationController(services.notificationService, services.db);
controllers.diagnosticController = new DiagnosticController(services.diagnosticService);
controllers.incidentController = new IncidentController(services.incidentService);
return controllers;
};
@@ -2,7 +2,7 @@ import { verifyJWT } from "../middleware/v1/verifyJWT.js";
import { authApiLimiter } from "../middleware/v1/rateLimiter.js";
import AuthRoutes from "../routes/v1/authRoute.js";
import InviteRoutes from "../routes/v1//inviteRoute.js";
import InviteRoutes from "../routes/v1/inviteRoute.js";
import MonitorRoutes from "../routes/v1/monitorRoute.js";
import CheckRoutes from "../routes/v1/checkRoute.js";
import SettingsRoutes from "../routes/v1/settingsRoute.js";
@@ -10,12 +10,12 @@ import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js";
import StatusPageRoutes from "../routes/v1/statusPageRoute.js";
import QueueRoutes from "../routes/v1/queueRoute.js";
import LogRoutes from "../routes/v1/logRoutes.js";
import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js";
import DiagnosticRoutes from "../routes/v1/diagnosticRoute.js";
import NotificationRoutes from "../routes/v1/notificationRoute.js";
import IncidentRoutes from "../routes/v1/incidentRoute.js";
export const setupRoutes = (app, controllers) => {
export const setupRoutes = (app: any, controllers: Record<string, any>) => {
// V1
const authRoutes = new AuthRoutes(controllers.authController);
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
+42 -42
View File
@@ -1,23 +1,23 @@
import ServiceRegistry from "../service/v1/system/serviceRegistry.js";
import TranslationService from "../service/v1/system/translationService.js";
import StringService from "../service/v1/system/stringService.js";
import MongoDB from "../db/v1/MongoDB.js";
import NetworkService from "../service/v1/infrastructure/networkService.js";
import EmailService from "../service/v1/infrastructure/emailService.js";
import BufferService from "../service/v1/infrastructure/bufferService.js";
import StatusService from "../service/v1/infrastructure/statusService.js";
import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js";
import NotificationService from "../service/v1/infrastructure/notificationService.js";
import ErrorService from "../service/v1/infrastructure/errorService.js";
import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import UserService from "../service/v1/business/userService.js";
import CheckService from "../service/v1/business/checkService.js";
import DiagnosticService from "../service/v1/business/diagnosticService.js";
import InviteService from "../service/v1/business/inviteService.js";
import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js";
import MonitorService from "../service/v1/business/monitorService.js";
import IncidentService from "../service/v1/business/incidentService.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import TranslationService from "../service/system/translationService.js";
import StringService from "../service/system/stringService.js";
import MongoDB from "../db/MongoDB.js";
import NetworkService from "../service/infrastructure/networkService.js";
import EmailService from "../service/infrastructure/emailService.js";
import BufferService from "../service/infrastructure/bufferService.js";
import StatusService from "../service/infrastructure/statusService.js";
import NotificationUtils from "../service/infrastructure/notificationUtils.js";
import NotificationService from "../service/infrastructure/notificationService.js";
import ErrorService from "../service/infrastructure/errorService.js";
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import UserService from "../service/business/userService.js";
import CheckService from "../service/business/checkService.js";
import DiagnosticService from "../service/business/diagnosticService.js";
import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import MonitorService from "../service/business/monitorService.js";
import IncidentService from "../service/business/incidentService.js";
import papaparse from "papaparse";
import axios from "axios";
import got from "got";
@@ -47,29 +47,29 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/v1/models/Check.js";
import Monitor from "../db/v1/models/Monitor.js";
import User from "../db/v1/models/User.js";
import InviteToken from "../db/v1/models/InviteToken.js";
import StatusPage from "../db/v1/models/StatusPage.js";
import Team from "../db/v1/models/Team.js";
import MaintenanceWindow from "../db/v1/models/MaintenanceWindow.js";
import MonitorStats from "../db/v1/models/MonitorStats.js";
import Notification from "../db/v1/models/Notification.js";
import RecoveryToken from "../db/v1/models/RecoveryToken.js";
import AppSettings from "../db/v1/models/AppSettings.js";
import Incident from "../db/v1/models/Incident.js";
import Check from "../db/models/Check.js";
import Monitor from "../db/models/Monitor.js";
import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js";
import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
import MonitorStats from "../db/models/MonitorStats.js";
import Notification from "../db/models/Notification.js";
import RecoveryToken from "../db/models/RecoveryToken.js";
import AppSettings from "../db/models/AppSettings.js";
import Incident from "../db/models/Incident.js";
import InviteModule from "../db/v1/modules/inviteModule.js";
import CheckModule from "../db/v1/modules/checkModule.js";
import StatusPageModule from "../db/v1/modules/statusPageModule.js";
import UserModule from "../db/v1/modules/userModule.js";
import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/v1/modules/monitorModule.js";
import NotificationModule from "../db/v1/modules/notificationModule.js";
import RecoveryModule from "../db/v1/modules/recoveryModule.js";
import SettingsModule from "../db/v1/modules/settingsModule.js";
import IncidentModule from "../db/v1/modules/incidentModule.js";
import InviteModule from "../db/modules/inviteModule.js";
import CheckModule from "../db/modules/checkModule.js";
import StatusPageModule from "../db/modules/statusPageModule.js";
import UserModule from "../db/modules/userModule.js";
import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/modules/monitorModule.js";
import NotificationModule from "../db/modules/notificationModule.js";
import RecoveryModule from "../db/modules/recoveryModule.js";
import SettingsModule from "../db/modules/settingsModule.js";
import IncidentModule from "../db/modules/incidentModule.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
+233
View File
@@ -0,0 +1,233 @@
import { Request, Response, NextFunction } from "express";
import { AppError } from "@/utils/AppError.js";
import {
registrationBodyValidation,
loginValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
editUserPasswordByIdBodyValidation,
} from "@/validation/joi.js";
const SERVICE_NAME = "authController";
class AuthController {
static SERVICE_NAME = SERVICE_NAME;
private userService: any;
constructor(userService: any) {
this.userService = userService;
}
get serviceName() {
return AuthController.SERVICE_NAME;
}
registerUser = async (req: Request, res: Response, next: NextFunction) => {
try {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await registrationBodyValidation.validateAsync(req.body);
const { user, token } = await this.userService.registerUser(req.body, req.file);
res.status(200).json({
success: true,
msg: "User registered successfully",
data: { user, token },
});
} catch (error) {
next(error);
}
};
loginUser = async (req: Request, res: Response, next: NextFunction) => {
try {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await loginValidation.validateAsync(req.body);
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
return res.status(200).json({
success: true,
msg: "User logged in successfully",
data: {
user,
token,
},
});
} catch (error) {
next(error);
}
};
editUser = async (req: Request, res: Response, next: NextFunction) => {
try {
await editUserBodyValidation.validateAsync(req.body);
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
res.status(200).json({
success: true,
msg: "User updated successfully",
data: updatedUser,
});
} catch (error) {
next(error);
}
};
checkSuperadminExists = async (req: Request, res: Response, next: NextFunction) => {
try {
const superAdminExists = await this.userService.checkSuperadminExists();
return res.status(200).json({
success: true,
msg: "Superadmin existence checked successfully",
data: superAdminExists,
});
} catch (error) {
next(error);
}
};
requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
try {
await recoveryValidation.validateAsync(req.body);
const email = req?.body?.email;
const msgId = await this.userService.requestRecovery(email);
return res.status(200).json({
success: true,
msg: "Password recovery email sent successfully",
data: msgId,
});
} catch (error) {
next(error);
}
};
validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
try {
await recoveryTokenBodyValidation.validateAsync(req.body);
await this.userService.validateRecovery(req.body.recoveryToken);
return res.status(200).json({
success: true,
msg: "Recovery token is valid",
});
} catch (error) {
next(error);
}
};
resetPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
await newPasswordValidation.validateAsync(req.body);
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
return res.status(200).json({
success: true,
msg: "Password has been reset successfully",
data: { user, token },
});
} catch (error) {
next(error);
}
};
deleteUser = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.userService.deleteUser(req.user);
return res.status(200).json({
success: true,
msg: "User deleted successfully",
});
} catch (error) {
next(error);
}
};
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const allUsers = await this.userService.getAllUsers();
return res.status(200).json({
success: true,
msg: "Users retrieved successfully",
data: allUsers,
});
} catch (error) {
next(error);
}
};
getUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
await getUserByIdParamValidation.validateAsync(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);
return res.status(200).json({ success: true, msg: "ok", data: user });
} catch (error) {
next(error);
}
};
editUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 });
}
const userId = req.params.userId;
const user = { ...req.body };
await editUserByIdParamValidation.validateAsync(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user._id) {
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
} else {
await editUserByIdBodyValidation.validateAsync(req.body);
}
await this.userService.editUserById(userId, user);
return res.status(200).json({ success: true, msg: "ok" });
} catch (error) {
next(error);
}
};
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
try {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 });
}
const userId = req.params.userId;
await editUserByIdParamValidation.validateAsync(req.params);
await editUserPasswordByIdBodyValidation.validateAsync(req.body);
const updatedPassword = req.body.password;
await this.userService.setPasswordByUserId(userId, updatedPassword);
return res.status(200).json({ success: true, msg: "Password reset successfully" });
} catch (error) {
next(error);
}
};
}
export default AuthController;
@@ -13,7 +13,7 @@ import {
getCertificateParamValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
} from "../../validation/joi.js";
} from "@/validation/joi.js";
import sslChecker from "ssl-checker";
import { fetchMonitorCertificate } from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
@@ -1,77 +0,0 @@
import { createAnnouncementValidation } from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "announcementController";
/**
* Controller for managing announcements in the system.
* This class handles the creation of new announcements.
*
* @class AnnouncementController
*/
class AnnouncementController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
constructor(commonDependencies) {
super(commonDependencies);
this.createAnnouncement = this.createAnnouncement.bind(this);
this.getAnnouncement = this.getAnnouncement.bind(this);
}
get serviceName() {
return AnnouncementController.SERVICE_NAME;
}
/**
* Handles the creation of a new announcement.
*
* @async
* @param {Object} req - The request object, containing the announcement data in the body.
* @param {Object} res - The response object used to send the result back to the client.
* @param {Function} next - The next middleware function in the stack for error handling.
*
* @returns {Promise<void>} A promise that resolves once the response is sent.
*/
createAnnouncement = asyncHandler(
async (req, res, next) => {
await createAnnouncementValidation.validateAsync(req.body);
const { title, message } = req.body;
const announcementData = {
title: title.trim(),
message: message.trim(),
userId: req.user._id,
};
const newAnnouncement = await this.db.createAnnouncement(announcementData);
return res.success({
msg: this.stringService.createAnnouncement,
data: newAnnouncement,
});
},
SERVICE_NAME,
"createAnnouncement"
);
/**
* Handles retrieving announcements with pagination.
*
* @async
* @param {Object} res - The response object used to send the result back to the client.
* - `data`: The list of announcements to be sent back to the client.
* - `msg`: A message about the success of the request.
* @param {Function} next - The next middleware function in the stack for error handling.
*/
getAnnouncement = asyncHandler(
async (req, res, next) => {
const allAnnouncements = await this.db.getAnnouncements();
return res.success({
msg: this.stringService.getAnnouncement,
data: allAnnouncements,
});
},
SERVICE_NAME,
"getAnnouncement"
);
}
export default AnnouncementController;
-478
View File
@@ -1,478 +0,0 @@
import BaseController from "./baseController.js";
import {
registrationBodyValidation,
loginValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
editUserPasswordByIdBodyValidation,
} from "../../validation/joi.js";
const SERVICE_NAME = "authController";
/**
* Authentication Controller
*
* Handles all authentication-related HTTP requests including user registration,
* login, password recovery, and user management operations.
*
* @class AuthController
* @description Manages user authentication and authorization operations
*/
class AuthController extends BaseController {
static SERVICE_NAME = SERVICE_NAME;
/**
* Creates an instance of AuthController.
*
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.settingsService - Service for application settings
* @param {Object} dependencies.emailService - Service for email operations
* @param {Object} dependencies.jobQueue - Service for job queue operations
* @param {Object} dependencies.userService - User business logic service
*/
constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.emailService = emailService;
this.jobQueue = jobQueue;
this.userService = userService;
}
get serviceName() {
return AuthController.SERVICE_NAME;
}
/**
* Registers a new user in the system.
*
* @async
* @function registerUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing user registration data
* @param {string} req.body.firstName - User's first name
* @param {string} req.body.lastName - User's last name
* @param {string} req.body.email - User's email address (will be converted to lowercase)
* @param {string} req.body.password - User's password
* @param {string} [req.body.inviteToken] - Invite token for registration (required if superadmin exists)
* @param {string} [req.body.teamId] - Team ID (auto-assigned if superadmin)
* @param {Array<string>} [req.body.role] - User roles (auto-assigned if superadmin)
* @param {Object} [req.file] - Profile image file uploaded via multer
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 409 - Conflict if user already exists
* @example
* // Register first user (becomes superadmin)
* POST /auth/register
* {
* "firstName": "John",
* "lastName": "Doe",
* "email": "john@example.com",
* "password": "SecurePass123!"
* }
*
* // Register subsequent user (requires invite token)
* POST /auth/register
* {
* "firstName": "Jane",
* "lastName": "Smith",
* "email": "jane@example.com",
* "password": "SecurePass123!",
* "inviteToken": "abc123..."
* }
*/
registerUser = this.asyncHandler(
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await registrationBodyValidation.validateAsync(req.body);
const { user, token } = await this.userService.registerUser(req.body, req.file);
res.success({
msg: this.stringService.authCreateUser,
data: { user, token },
});
},
SERVICE_NAME,
"registerUser"
);
/**
* Authenticates a user and returns a JWT token.
*
* @async
* @function loginUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing login credentials
* @param {string} req.body.email - User's email address (will be converted to lowercase)
* @param {string} req.body.password - User's password
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 401 - Unauthorized if credentials are incorrect
* @example
* POST /auth/login
* {
* "email": "john@example.com",
* "password": "SecurePass123!"
* }
*/
loginUser = this.asyncHandler(
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await loginValidation.validateAsync(req.body);
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
return res.success({
msg: this.stringService.authLoginUser,
data: {
user,
token,
},
});
},
SERVICE_NAME,
"loginUser"
);
/**
* Updates the current user's profile information.
*
* @async
* @function editUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing user update data
* @param {string} [req.body.firstName] - Updated first name
* @param {string} [req.body.lastName] - Updated last name
* @param {string} [req.body.password] - Current password (required for password change)
* @param {string} [req.body.newPassword] - New password (required for password change)
* @param {boolean} [req.body.deleteProfileImage] - Flag to delete profile image
* @param {Object} [req.file] - New profile image file
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with updated user data
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 403 - Forbidden if current password is incorrect
* @example
* PUT /auth/user
* {
* "firstName": "John Updated",
* "lastName": "Doe Updated"
* }
*
* // Change password
* PUT /auth/user
* {
* "password": "OldPass123!",
* "newPassword": "NewPass123!"
* }
*/
editUser = this.asyncHandler(
async (req, res) => {
await editUserBodyValidation.validateAsync(req.body);
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
res.success({
msg: this.stringService.authUpdateUser,
data: updatedUser,
});
},
SERVICE_NAME,
"editUser"
);
/**
* Checks if a superadmin account exists in the system.
*
* @async
* @function checkSuperadminExists
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with boolean indicating superadmin existence
* @example
* GET /auth/users/superadmin
* // Response: { "data": true } or { "data": false }
*/
checkSuperadminExists = this.asyncHandler(
async (req, res) => {
const superAdminExists = await this.userService.checkSuperadminExists();
return res.success({
msg: this.stringService.authAdminExists,
data: superAdminExists,
});
},
SERVICE_NAME,
"checkSuperadminExists"
);
/**
* Initiates password recovery process by sending a recovery email.
*
* @async
* @function requestRecovery
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing email
* @param {string} req.body.email - Email address for password recovery
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with message ID
* @throws {Error} 422 - Validation error if email is invalid
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* POST /auth/recovery/request
* {
* "email": "john@example.com"
* }
*/
requestRecovery = this.asyncHandler(
async (req, res) => {
await recoveryValidation.validateAsync(req.body);
const email = req?.body?.email;
const msgId = await this.userService.requestRecovery(email);
return res.success({
msg: this.stringService.authCreateRecoveryToken,
data: msgId,
});
},
SERVICE_NAME,
"requestRecovery"
);
/**
* Validates a password recovery token.
*
* @async
* @function validateRecovery
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing recovery token
* @param {string} req.body.recoveryToken - Recovery token to validate
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response if token is valid
* @throws {Error} 422 - Validation error if token format is invalid
* @throws {Error} 400 - Bad request if token is invalid or expired
* @example
* POST /auth/recovery/validate
* {
* "recoveryToken": "abc123..."
* }
*/
validateRecovery = this.asyncHandler(
async (req, res) => {
await recoveryTokenBodyValidation.validateAsync(req.body);
await this.userService.validateRecovery(req.body.recoveryToken);
return res.success({
msg: this.stringService.authVerifyRecoveryToken,
});
},
SERVICE_NAME,
"validateRecovery"
);
/**
* Resets user password using a valid recovery token.
*
* @async
* @function resetPassword
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing new password and recovery token
* @param {string} req.body.password - New password
* @param {string} req.body.recoveryToken - Valid recovery token
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if password format is invalid
* @throws {Error} 400 - Bad request if token is invalid or expired
* @example
* POST /auth/recovery/reset
* {
* "password": "NewSecurePass123!",
* "recoveryToken": "abc123..."
* }
*/
resetPassword = this.asyncHandler(
async (req, res) => {
await newPasswordValidation.validateAsync(req.body);
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
return res.success({
msg: this.stringService.authResetPassword,
data: { user, token },
});
},
SERVICE_NAME,
"resetPassword"
);
/**
* Deletes the current user's account and associated data.
*
* @async
* @function deleteUser
* @param {Object} req - Express request object
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user._id - User ID
* @param {string} req.user.email - User email
* @param {string} req.user.teamId - User's team ID
* @param {Array<string>} req.user.role - User roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming user deletion
* @throws {Error} 400 - Bad request if user is demo user
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* DELETE /auth/user
* // Requires JWT authentication
*/
deleteUser = this.asyncHandler(
async (req, res) => {
await this.userService.deleteUser(req.user);
return res.success({
msg: this.stringService.authDeleteUser,
});
},
SERVICE_NAME,
"deleteUser"
);
/**
* Retrieves all users in the system (admin/superadmin only).
*
* @async
* @function getAllUsers
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with array of users
* @throws {Error} 403 - Forbidden if user doesn't have admin/superadmin role
* @example
* GET /auth/users
* // Requires JWT authentication with admin/superadmin role
*/
getAllUsers = this.asyncHandler(
async (req, res) => {
const allUsers = await this.userService.getAllUsers();
return res.success({
msg: this.stringService.authGetAllUsers,
data: allUsers,
});
},
SERVICE_NAME,
"getAllUsers"
);
/**
* Retrieves a specific user by ID (superadmin only).
*
* @async
* @function getUserById
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.userId - ID of the user to retrieve
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {Array<string>} req.user.role - Current user's roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data
* @throws {Error} 422 - Validation error if userId is invalid
* @throws {Error} 403 - Forbidden if user doesn't have superadmin role
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* GET /auth/users/507f1f77bcf86cd799439011
* // Requires JWT authentication with superadmin role
*/
getUserById = this.asyncHandler(
async (req, res) => {
await getUserByIdParamValidation.validateAsync(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);
return res.success({ msg: "ok", data: user });
},
SERVICE_NAME,
"getUserById"
);
/**
* Updates a specific user by ID (superadmin only).
*
* @async
* @function editUserById
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.userId - ID of the user to update
* @param {Object} req.body - Request body containing user update data
* @param {string} [req.body.firstName] - Updated first name
* @param {string} [req.body.lastName] - Updated last name
* @param {Array<string>} [req.body.role] - Updated user roles
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user._id - Current user's ID
* @param {Array<string>} req.user.role - Current user's roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming user update
* @throws {Error} 422 - Validation error if parameters or body are invalid
* @throws {Error} 403 - Forbidden if user doesn't have superadmin role
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* PUT /auth/users/507f1f77bcf86cd799439011
* {
* "firstName": "Updated Name",
* "role": ["admin"]
* }
* // Requires JWT authentication with superadmin role
*/
editUserById = this.asyncHandler(
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
throw this.errorService.createError("Unauthorized", 403);
}
const userId = req.params.userId;
const user = { ...req.body };
await editUserByIdParamValidation.validateAsync(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user._id) {
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
} else {
await editUserByIdBodyValidation.validateAsync(req.body);
}
await this.userService.editUserById(userId, user);
return res.success({ msg: "ok" });
},
SERVICE_NAME,
"editUserById"
);
editUserPasswordById = this.asyncHandler(
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
throw this.errorService.createError("Unauthorized", 403);
}
const userId = req.params.userId;
await editUserByIdParamValidation.validateAsync(req.params);
await editUserPasswordByIdBodyValidation.validateAsync(req.body);
const updatedPassword = req.body.password;
await this.userService.setPasswordByUserId(userId, updatedPassword);
return res.success({ msg: "Password reset successfully" });
},
SERVICE_NAME,
"editUserPasswordById"
);
}
export default AuthController;
@@ -1,83 +0,0 @@
import { AppError } from "../../service/v1/infrastructure/errorService.js";
export const createCommonDependencies = (db, errorService, logger, stringService) => {
return {
db,
errorService,
logger,
stringService,
};
};
class BaseController {
constructor({ db, logger, errorService, ...additionalDependencies }) {
this.db = db;
this.logger = logger;
this.errorService = errorService;
Object.assign(this, additionalDependencies);
this.asyncHandler = (fn, serviceName, methodName) => {
return async (req, res, next) => {
try {
await fn(req, res, next);
} catch (error) {
// Handle validation errors
if (error.isJoi) {
const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName);
return next(validationError);
}
if (error.name === "ValidationError") {
const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName);
return next(validationError);
}
if (error.name === "CastError") {
const notFoundError = this.errorService.createNotFoundError(
"Invalid resource identifier",
{ field: error.path, value: error.value },
serviceName,
methodName
);
return next(notFoundError);
}
if (error.code === "11000") {
const conflictError = this.errorService.createConflictError("Resource already exists", {
originalError: error.message,
code: error.code,
});
conflictError.service = serviceName;
conflictError.method = methodName;
return next(conflictError);
}
if (error instanceof AppError) {
error.service = error.service || serviceName;
error.method = error.method || methodName;
return next(error);
}
if (error.status) {
const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, {
originalError: error.message,
stack: error.stack,
});
return next(appError);
}
// For unknown errors, create a server error
const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", {
originalError: error.message,
stack: error.stack,
});
appError.service = serviceName;
appError.method = methodName;
appError.stack = error.stack; // Preserve original stack
return next(appError);
}
};
};
}
}
export default BaseController;
File renamed without changes.
@@ -0,0 +1,10 @@
import Monitor from "@/db/models/Monitor.js";
async function migrateStatusWindowThreshold() {
const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });
for (const monitor of monitors) {
monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100;
await monitor.save();
}
}
export { migrateStatusWindowThreshold };
@@ -0,0 +1,93 @@
import mongoose from "mongoose";
const CHECKS_COLLECTION = "checks";
const BACKUP_COLLECTION = "checks_backup";
const BATCH_SIZE = 1000;
const getDb = () => {
const db = mongoose.connection.db;
if (!db) {
throw new Error("Database connection is not initialized");
}
return db;
};
const backupAndDropExistingCollection = async () => {
const db = getDb();
const collections = await db.listCollections({ name: CHECKS_COLLECTION }).toArray();
if (collections.length === 0) {
return false;
}
await db.collection(CHECKS_COLLECTION).aggregate([
{ $match: {} },
{ $out: BACKUP_COLLECTION },
]).toArray();
await db.collection(CHECKS_COLLECTION).drop();
return true;
};
const createTimeSeriesCollection = async () => {
const db = getDb();
const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray();
if (existing.length === 0) {
await db.createCollection(CHECKS_COLLECTION, {
timeseries: {
timeField: "createdAt",
metaField: "metadata",
granularity: "seconds",
},
});
}
await db.collection(CHECKS_COLLECTION).createIndexes([
{ key: { updatedAt: 1 } },
{ key: { "metadata.monitorId": 1, updatedAt: 1 } },
{ key: { "metadata.monitorId": 1, updatedAt: -1 } },
{ key: { "metadata.teamId": 1, updatedAt: -1 } },
]);
};
const migrateBackupData = async () => {
const db = getDb();
const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray();
if (backupExists.length === 0) {
return;
}
const source = db.collection(BACKUP_COLLECTION);
const target = db.collection(CHECKS_COLLECTION);
const cursor = source.find();
const operations: any[] = [];
while (await cursor.hasNext()) {
const doc = await cursor.next();
if (!doc) {
continue;
}
const metadata = {
monitorId: doc.monitorId,
teamId: doc.teamId,
type: doc.type,
};
operations.push({ insertOne: { document: { ...doc, metadata } } });
if (operations.length >= BATCH_SIZE) {
await target.bulkWrite(operations);
operations.length = 0;
}
}
if (operations.length) {
await target.bulkWrite(operations);
}
};
export const convertChecksToTimeSeries = async () => {
const backedUp = await backupAndDropExistingCollection();
await createTimeSeriesCollection();
if (backedUp) {
await migrateBackupData();
}
};
+51
View File
@@ -0,0 +1,51 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import MigrationModel from "../models/Migration.js";
type MigrationEntry = {
name: string;
execute: () => Promise<void>;
};
const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {
try {
logger?.info({ message: "Running migrations", service: "Migrations" });
for (const migration of migrations) {
const exists = await MigrationModel.findOne({ name: migration.name, status: "completed" });
if (exists) {
logger?.info({ message: `Skipping ${migration.name}`, service: "Migrations" });
continue;
}
try {
await migration.execute();
await MigrationModel.findOneAndUpdate(
{ name: migration.name },
{ status: "completed", completedAt: new Date(), error: undefined },
{ upsert: true }
);
logger?.info({ message: `Completed ${migration.name}`, service: "Migrations" });
} catch (error) {
const err = error as Error;
await MigrationModel.findOneAndUpdate(
{ name: migration.name },
{ status: "failed", error: err?.message },
{ upsert: true }
);
throw error;
}
}
logger?.info({ message: "Migrations completed", service: "Migrations" });
} catch (error) {
const err = error as Error;
logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack });
throw error;
}
};
export { runMigrations };
File renamed without changes.
+214
View File
@@ -0,0 +1,214 @@
import mongoose from "mongoose";
import { MonitorTypes } from "@/types/monitor.js";
const cpuSchema = new mongoose.Schema(
{
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
},
{ _id: false }
);
const memorySchema = new mongoose.Schema(
{
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
},
{ _id: false }
);
const diskSchema = new mongoose.Schema(
{
device: { type: String, default: "" },
mountpoint: { type: String, default: "" },
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
},
{ _id: false }
);
const hostSchema = new mongoose.Schema(
{
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
},
{ _id: false }
);
const errorSchema = new mongoose.Schema(
{
metric: { type: [String], default: [] },
err: { type: String, default: "" },
},
{ _id: false }
);
const captureSchema = new mongoose.Schema(
{
version: { type: String, default: "" },
mode: { type: String, default: "" },
},
{ _id: false }
);
const networkInterfaceSchema = new mongoose.Schema(
{
name: { type: String },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
},
{ _id: false }
);
const metadataSchema = new mongoose.Schema(
{
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
index: true,
},
type: {
type: String,
enum: MonitorTypes,
required: true,
index: true,
},
},
{ _id: false }
);
const CheckSchema = new mongoose.Schema(
{
metadata: {
type: metadataSchema,
required: true,
},
status: {
type: Boolean,
index: true,
},
responseTime: {
type: Number,
},
timings: {
type: Object,
default: {},
},
statusCode: {
type: Number,
index: true,
},
message: {
type: String,
},
expiry: {
type: Date,
default: Date.now,
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
},
// Hardware fields
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [diskSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
},
// PageSpeed fields
accessibility: {
type: Number,
},
bestPractices: {
type: Number,
},
seo: {
type: Number,
},
performance: {
type: Number,
},
audits: {
type: Object,
},
},
{
timestamps: true,
strict: false,
timeseries: {
timeField: "createdAt",
metaField: "metadata",
granularity: "seconds",
},
}
);
CheckSchema.index({ updatedAt: 1 });
CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 });
CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 });
CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 });
export default mongoose.model("Check", CheckSchema);
File renamed without changes.
File renamed without changes.
+13
View File
@@ -0,0 +1,13 @@
import mongoose from "mongoose";
const MigrationSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
status: { type: String, enum: ["completed", "failed"], default: "completed" },
completedAt: { type: Date },
error: { type: String },
},
{ timestamps: true }
);
export default mongoose.model("Migration", MigrationSchema);
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,18 +0,0 @@
// import Monitor from "@/db/v1/models/Monitor.js";
async function migrateStatusWindowThreshold() {
try {
// const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });
// for (const monitor of monitors) {
// monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100;
// await monitor.save();
// console.log(`Migrated monitor ${monitor._id}: statusWindowThreshold set to ${monitor.statusWindowThreshold}`);
// }
// console.log("StatusWindowThreshold migration complete.");
return true;
} catch (err) {
console.error("Migration error:", err);
return false;
}
}
export { migrateStatusWindowThreshold };
-7
View File
@@ -1,7 +0,0 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
const runMigrations = async () => {
await migrateStatusWindowThreshold();
};
export { runMigrations };
-178
View File
@@ -1,178 +0,0 @@
import mongoose from "mongoose";
const cpuSchema = mongoose.Schema({
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const memorySchema = mongoose.Schema({
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const diskSchema = mongoose.Schema({
device: { type: String, default: "" },
mountpoint: { type: String, default: "" },
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const hostSchema = mongoose.Schema({
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
});
const errorSchema = mongoose.Schema({
metric: { type: [String], default: [] },
err: { type: String, default: "" },
});
const captureSchema = mongoose.Schema({
version: { type: String, default: "" },
mode: { type: String, default: "" },
});
const networkInterfaceSchema = mongoose.Schema({
name: { type: String },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
});
const CheckSchema = new mongoose.Schema(
{
// Common fields
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
index: true,
},
type: {
type: String,
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"],
required: true,
index: true,
},
status: {
type: Boolean,
index: true,
},
responseTime: {
type: Number,
},
timings: {
type: Object,
default: {},
},
statusCode: {
type: Number,
index: true,
},
message: {
type: String,
},
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24 * 30, // 30 days
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
},
// Hardware fields
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [diskSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
},
// PageSpeed fields
accessibility: {
type: Number,
},
bestPractices: {
type: Number,
},
seo: {
type: Number,
},
performance: {
type: Number,
},
audits: {
type: Object,
},
},
{ timestamps: true }
);
CheckSchema.index({ updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
CheckSchema.index({ teamId: 1, updatedAt: -1 });
export default mongoose.model("Check", CheckSchema);
+5 -2
View File
@@ -5,10 +5,11 @@ import { initShutdownListener } from "./shutdown.js";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
import { runMigrations } from "./db/migration/index.js";
import Logger from "./utils/logger.js";
import SettingsService from "./service/v1/system/settingsService.js";
import AppSettings from "./db/v1/models/AppSettings.js";
import SettingsService from "./service/system/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
const SERVICE_NAME = "Server";
let logger;
@@ -30,6 +31,8 @@ const startApp = async () => {
// Initialize services
const services = await initializeServices({ logger, envSettings, settingsService });
await runMigrations(logger);
// Initialize controllers
const controllers = initializeControllers(services);
+2 -2
View File
@@ -1,6 +1,6 @@
import { logger } from "../../utils/logger.js";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import StringService from "../../service/v1/system/stringService.js";
import ServiceRegistry from "../../service/system/serviceRegistry.js";
import StringService from "../../service/system/stringService.js";
const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
+3 -3
View File
@@ -1,9 +1,9 @@
import jwt from "jsonwebtoken";
const TOKEN_PREFIX = "Bearer ";
const SERVICE_NAME = "allowedRoles";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import StringService from "../../service/v1/system/stringService.js";
import SettingsService from "../../service/v1/system/settingsService.js";
import ServiceRegistry from "../../service/system/serviceRegistry.js";
import StringService from "../../service/system/stringService.js";
import SettingsService from "../../service/system/settingsService.js";
const isAllowed = (allowedRoles) => {
return (req, res, next) => {
+3 -3
View File
@@ -1,7 +1,7 @@
import jwt from "jsonwebtoken";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import SettingsService from "../../service/v1/system/settingsService.js";
import StringService from "../../service/v1/system/stringService.js";
import ServiceRegistry from "../../service/system/serviceRegistry.js";
import SettingsService from "../../service/system/settingsService.js";
import StringService from "../../service/system/stringService.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
+2 -2
View File
@@ -1,6 +1,6 @@
import { logger } from "../../utils/logger.js";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import StringService from "../../service/v1/system/stringService.js";
import ServiceRegistry from "../../service/system/serviceRegistry.js";
import StringService from "../../service/system/stringService.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "verifyOwnership";
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,9 @@
import type { Monitor } from "@/types/index.js";
export interface IMonitorsRepository {
// create
// single fetch
// collection fetch
findAll(): Promise<Monitor[] | null>;
// update
// delete
}
@@ -0,0 +1,5 @@
class MongoMonitorsRepository implements IMonitorsRepository {
findAll = async () => {
return null;
};
}
@@ -1,33 +0,0 @@
import { Router } from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
import { isAllowed } from "../middleware/isAllowed.js";
class AnnouncementRoutes {
constructor(controller) {
this.router = Router();
this.announcementController = controller;
this.initRoutes();
}
initRoutes() {
/**
* @route POST /
* @desc Create a new announcement
* @access Private (Requires JWT verification)
*/
this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.announcementController.createAnnouncement);
/**
* @route GET /
* @desc Get announcements
* @access Public
*/
this.router.get("/", this.announcementController.getAnnouncement);
}
getRouter() {
return this.router;
}
}
export default AnnouncementRoutes;
+1 -1
View File
@@ -1,7 +1,7 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
import multer from "multer";
import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js";
import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
@@ -1,4 +1,4 @@
import { createMonitorsBodyValidation } from "../../../validation/joi.js";
import { createMonitorsBodyValidation } from "@/validation/joi.js";
const SERVICE_NAME = "MonitorService";
class MonitorService {
@@ -1,5 +1,5 @@
import MonitorStats from "../../../db/v1/models/MonitorStats.js";
import Check from "../../../db/v1/models/Check.js";
import MonitorStats from "../../db/models/MonitorStats.js";
import Check from "../../db/models/Check.js";
const SERVICE_NAME = "StatusService";
class StatusService {
@@ -337,9 +337,11 @@ class StatusService {
} = networkResponse;
const check = {
monitorId,
teamId,
type,
metadata: {
monitorId,
teamId,
type,
},
status,
statusCode: code,
responseTime,
+79
View File
@@ -0,0 +1,79 @@
import type { MonitorType } from "@/types/index.js";
export interface CheckTimingPhases {
wait: number;
dns: number;
tcp: number;
tls: number;
request: number;
firstByte: number;
download: number;
total: number;
}
export interface CheckTimings {
start: number;
socket: number;
lookup: number;
connect: number;
secureConnect: number;
upload: number;
response: number;
end: number;
phases: CheckTimingPhases;
}
export interface CheckCpuInfo {
physical_core: number;
logical_core: number;
frequency: number;
temperature: number[];
free_percent: number;
usage_percent: number;
}
export interface CheckMemoryInfo {
total_bytes: number;
available_bytes: number;
used_bytes: number;
usage_percent: number;
}
export interface CheckHostInfo {
os: string;
platform: string;
kernel_version: string;
}
export interface CheckCaptureInfo {
version: string;
mode: string;
}
export interface CheckDiskInfo {}
export interface CheckNetInfo {}
export interface Check {
id: string;
monitorId: string;
teamId: string;
type: MonitorType;
status: boolean;
responseTime: number;
timings: CheckTimings;
statusCode: number;
message: string;
ack: boolean;
expiry: string;
cpu: CheckCpuInfo;
memory: CheckMemoryInfo;
disk: CheckDiskInfo[];
host: CheckHostInfo;
errors: string[];
capture: CheckCaptureInfo;
net: CheckNetInfo[];
__v: number;
createdAt: string;
updatedAt: string;
}
+2
View File
@@ -0,0 +1,2 @@
export * from "@/types/check.js";
export * from "@/types/monitor.js";
+29
View File
@@ -0,0 +1,29 @@
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export interface Monitor {
id: string;
userId: string;
teamId: string;
name: string;
description: string;
statusWindow: boolean[];
statusWindowSize: number;
statusWindowThreshold: number;
type: MonitorType;
ignoreTlsErrors: boolean;
url: string;
isActive: boolean;
interval: number;
notifications: string[];
alertThreshold: number;
selectedDisks: string[];
group: string | null;
cpuAlertThreshold: number;
memoryAlertThreshold: number;
diskAlertThreshold: number;
tempAlertThreshold: number;
createdAt: string;
updatedAt: string;
status: boolean;
}