mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-19 07:58:46 -05:00
Merge upstream/develop into fix/3120-data-leak
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
rootDir: ".",
|
||||
testEnvironment: "node",
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
transform: {
|
||||
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
testMatch: ["<rootDir>/test/**/*.test.ts"],
|
||||
setupFilesAfterEnv: [],
|
||||
collectCoverageFrom: ["src/**/*.ts"],
|
||||
coveragePathIgnorePatterns: ["/node_modules/", "/test/"],
|
||||
clearMocks: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+3406
-353
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "c8 mocha",
|
||||
"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",
|
||||
@@ -58,19 +58,27 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/dockerode": "^4.0.0",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/gamedig": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/mjml": "^4.7.4",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/ping": "0.4.4",
|
||||
"c8": "10.1.3",
|
||||
"chai": "5.2.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"esm": "3.2.25",
|
||||
"globals": "^15.14.0",
|
||||
"mocha": "11.1.0",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"prettier": "^3.3.3",
|
||||
"sinon": "19.0.2",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.5",
|
||||
"typescript": "5.9.2"
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import mongoose from "mongoose";
|
||||
import { MonitorModel } from "../dist/db/models/Monitor.js";
|
||||
import { CheckModel } from "../dist/db/models/Check.js";
|
||||
|
||||
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
||||
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
|
||||
const DEFAULT_USER_ID = "0000000000000000000000bb";
|
||||
const DEFAULT_MONITOR_TYPE = "http";
|
||||
const DEFAULT_TOTAL = 1_000_000;
|
||||
const DEFAULT_BATCH_SIZE = 5_000;
|
||||
|
||||
const parseObjectId = (value, fallback) => {
|
||||
try {
|
||||
return new mongoose.Types.ObjectId(value || fallback);
|
||||
} catch (error) {
|
||||
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
||||
return new mongoose.Types.ObjectId(fallback);
|
||||
}
|
||||
};
|
||||
|
||||
async function ensureMonitor({ monitorId, teamId, userId, type }) {
|
||||
const existing = await MonitorModel.findById(monitorId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
console.log(`Monitor ${monitorId.toString()} not found, creating it.`);
|
||||
const monitor = new MonitorModel({
|
||||
_id: monitorId,
|
||||
userId,
|
||||
teamId,
|
||||
name: `Seed Monitor ${monitorId.toString()}`,
|
||||
description: "Synthetic monitor for performance testing",
|
||||
statusWindow: [],
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
type,
|
||||
ignoreTlsErrors: false,
|
||||
url: "https://example.com",
|
||||
isActive: true,
|
||||
interval: 60000,
|
||||
alertThreshold: 5,
|
||||
cpuAlertThreshold: 5,
|
||||
memoryAlertThreshold: 5,
|
||||
diskAlertThreshold: 5,
|
||||
tempAlertThreshold: 5,
|
||||
selectedDisks: [],
|
||||
});
|
||||
|
||||
await monitor.save();
|
||||
return monitor;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db";
|
||||
const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID);
|
||||
const teamId = parseObjectId("69648b0578209af45f9ffe30");
|
||||
const userId = parseObjectId("69648b0678209af45f9ffe32");
|
||||
const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE;
|
||||
const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL);
|
||||
const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE);
|
||||
|
||||
console.log(`Connecting to MongoDB at ${mongoUri}`);
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
await ensureMonitor({ monitorId, teamId, userId, type: monitorType });
|
||||
|
||||
console.log(`Seeding ${total} checks for monitor ${monitorId.toString()} (team ${teamId.toString()}) in batches of ${batchSize}.`);
|
||||
|
||||
const docs = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
const baseTime = Date.now() - (total - i) * 1000;
|
||||
const createdAt = new Date(baseTime);
|
||||
docs.push({
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
type: monitorType,
|
||||
},
|
||||
status: i % 50 !== 0,
|
||||
statusCode: i % 50 !== 0 ? 200 : 500,
|
||||
responseTime: Math.floor(Math.random() * 1000),
|
||||
message: i % 50 !== 0 ? "OK" : "Error",
|
||||
expiry: createdAt,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
timings: {
|
||||
start: baseTime,
|
||||
socket: baseTime,
|
||||
lookup: baseTime,
|
||||
connect: baseTime,
|
||||
secureConnect: baseTime,
|
||||
upload: baseTime,
|
||||
response: baseTime + 40,
|
||||
end: baseTime + 45,
|
||||
phases: {
|
||||
wait: 0,
|
||||
dns: 1,
|
||||
tcp: 2,
|
||||
tls: 4,
|
||||
request: 0,
|
||||
firstByte: 30,
|
||||
download: 5,
|
||||
total: 45,
|
||||
},
|
||||
},
|
||||
cpu: {
|
||||
physical_core: 8,
|
||||
logical_core: 16,
|
||||
frequency: 3600,
|
||||
temperature: [50 + Math.random() * 10],
|
||||
free_percent: 40,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 32 * 1024 ** 3,
|
||||
available_bytes: 16 * 1024 ** 3,
|
||||
used_bytes: 16 * 1024 ** 3,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
device: "/dev/sda1",
|
||||
mountpoint: "/",
|
||||
read_speed_bytes: Math.random() * 10_000_000,
|
||||
write_speed_bytes: Math.random() * 10_000_000,
|
||||
total_bytes: 512 * 1024 ** 3,
|
||||
free_bytes: 128 * 1024 ** 3,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: "linux",
|
||||
platform: "ubuntu",
|
||||
kernel_version: "5.15.0",
|
||||
},
|
||||
net: [
|
||||
{
|
||||
name: "eth0",
|
||||
bytes_sent: Math.random() * 10_000_000,
|
||||
bytes_recv: Math.random() * 10_000_000,
|
||||
packets_sent: Math.random() * 1_000_000,
|
||||
packets_recv: Math.random() * 1_000_000,
|
||||
err_in: 0,
|
||||
err_out: 0,
|
||||
drop_in: 0,
|
||||
drop_out: 0,
|
||||
fifo_in: 0,
|
||||
fifo_out: 0,
|
||||
},
|
||||
],
|
||||
errors: i % 50 === 0 ? [{ metric: ["uptime"], err: "500" }] : [],
|
||||
});
|
||||
|
||||
if (docs.length === batchSize) {
|
||||
await CheckModel.insertMany(docs, { ordered: false });
|
||||
console.log(`Inserted ${i + 1} / ${total}`);
|
||||
docs.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (docs.length > 0) {
|
||||
await CheckModel.insertMany(docs, { ordered: false });
|
||||
}
|
||||
|
||||
await mongoose.disconnect();
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`Finished inserting ${total} checks in ${duration}s`);
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Failed to seed checks", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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/index.js";
|
||||
import IncidentService from "../service/business/incidentService.js";
|
||||
import papaparse from "papaparse";
|
||||
import axios from "axios";
|
||||
import got from "got";
|
||||
@@ -34,9 +34,8 @@ const { compile } = pkg;
|
||||
import mjml2html from "mjml";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { games } from "gamedig";
|
||||
import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
import { GameDig } from "gamedig";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { ObjectId } from "mongodb";
|
||||
@@ -47,33 +46,35 @@ 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 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 }) => {
|
||||
// repositories
|
||||
import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js";
|
||||
|
||||
export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => {
|
||||
const serviceRegistry = new ServiceRegistry({ logger });
|
||||
ServiceRegistry.instance = serviceRegistry;
|
||||
(ServiceRegistry as any).instance = serviceRegistry;
|
||||
|
||||
const translationService = new TranslationService(logger);
|
||||
await translationService.initialize();
|
||||
@@ -81,7 +82,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
const stringService = new StringService(translationService);
|
||||
|
||||
// Create DB
|
||||
const checkModule = new CheckModule({ logger, Check, Monitor, User });
|
||||
const checkModule = new CheckModule({ logger, Monitor, User });
|
||||
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
|
||||
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings });
|
||||
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
|
||||
@@ -89,7 +90,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
const monitorModule = new MonitorModule({
|
||||
Monitor,
|
||||
MonitorStats,
|
||||
Check,
|
||||
stringService,
|
||||
fs,
|
||||
path,
|
||||
@@ -120,6 +120,11 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
|
||||
await db.connect();
|
||||
|
||||
// Repositories
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository();
|
||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
|
||||
const networkService = new NetworkService({
|
||||
axios,
|
||||
got,
|
||||
@@ -146,7 +151,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
|
||||
const bufferService = new BufferService({ db, logger, envSettings, incidentService });
|
||||
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService });
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository });
|
||||
|
||||
const notificationUtils = new NotificationUtils({
|
||||
stringService,
|
||||
@@ -176,6 +181,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
db,
|
||||
logger,
|
||||
helper: superSimpleQueueHelper,
|
||||
monitorsRepository,
|
||||
});
|
||||
|
||||
// Business services
|
||||
@@ -212,7 +218,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
});
|
||||
const monitorService = new MonitorService({
|
||||
db,
|
||||
settingsService,
|
||||
jobQueue: superSimpleQueue,
|
||||
stringService,
|
||||
emailService,
|
||||
@@ -220,6 +225,9 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
logger,
|
||||
errorService,
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
});
|
||||
|
||||
const services = {
|
||||
Executable
+233
@@ -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;
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { type MonitorType, MonitorTypes } from "@/types/index.js";
|
||||
|
||||
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
|
||||
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
|
||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||
throw new Error("Certificate not found");
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
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 => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
if (!MonitorTypes.includes(input as MonitorType)) {
|
||||
throw new AppError({ message: `Invalid monitor type: ${input}`, status: 400 });
|
||||
}
|
||||
return input as MonitorType;
|
||||
};
|
||||
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => parseSingle(entry));
|
||||
}
|
||||
return parseSingle(value);
|
||||
};
|
||||
|
||||
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "asc" || value === "desc") {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
|
||||
};
|
||||
|
||||
const requireTeamId = (teamId?: string): string => {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
return teamId;
|
||||
};
|
||||
|
||||
export {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
};
|
||||
+85
-126
@@ -5,26 +5,35 @@ import {
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdParamValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorsWithChecksQueryValidation,
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
pauseMonitorParamValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
} from "../../validation/joi.js";
|
||||
} from "@/validation/joi.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import { fetchMonitorCertificate } from "./controllerUtils.js";
|
||||
import {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
} from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private monitorService: any;
|
||||
private monitorService: IMonitorService;
|
||||
|
||||
constructor(monitorService: any) {
|
||||
constructor(monitorService: IMonitorService) {
|
||||
this.monitorService = monitorService;
|
||||
}
|
||||
|
||||
@@ -33,33 +42,17 @@ class MonitorController {
|
||||
}
|
||||
|
||||
async verifyTeamAccess(teamId: string, monitorId: string) {
|
||||
const monitor = await this.monitorService.getMonitorById(monitorId);
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
if (monitor.teamId !== teamId) {
|
||||
throw new AppError({ message: "Access denied", status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
getAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const monitors = await this.monitorService.getAllMonitors();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Retrieved all monitors successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getCertificateParamValidation.validateAsync(req.params);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const { monitorId } = 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 certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
|
||||
@@ -77,15 +70,10 @@ class MonitorController {
|
||||
|
||||
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const monitorId = req?.params?.monitorId;
|
||||
const dateRange = req?.query?.dateRange;
|
||||
const normalize = req?.query?.normalize;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
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 data = await this.monitorService.getUptimeDetailsById({
|
||||
teamId,
|
||||
@@ -103,50 +91,14 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorStatsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const monitorId = req?.params?.monitorId;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const monitorStats = await this.monitorService.getMonitorStatsById({
|
||||
teamId,
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Monitor stats retrieved successfully",
|
||||
data: monitorStats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const monitorId = req?.params?.monitorId;
|
||||
const dateRange = req?.query?.dateRange;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.getHardwareDetailsById({
|
||||
teamId,
|
||||
@@ -163,18 +115,40 @@ class MonitorController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.getPageSpeedDetailsById({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Page speed details retrieved successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -190,10 +164,10 @@ class MonitorController {
|
||||
try {
|
||||
await createMonitorBodyValidation.validateAsync(req.body);
|
||||
|
||||
const userId = req?.user?._id;
|
||||
const teamId = req?.user?.teamId;
|
||||
const userId = requireString(req?.user?._id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body });
|
||||
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -219,19 +193,15 @@ class MonitorController {
|
||||
throw new AppError({ message: "File is empty", status: 400 });
|
||||
}
|
||||
|
||||
const userId = req?.user?._id;
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!userId || !teamId) {
|
||||
throw new AppError({ message: "Missing userId or teamId", status: 400 });
|
||||
}
|
||||
const userId = requireString(req?.user?._id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const fileData = req?.file?.buffer?.toString("utf-8");
|
||||
if (!fileData) {
|
||||
throw new AppError({ message: "Cannot get file from buffer", status: 400 });
|
||||
}
|
||||
|
||||
const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId });
|
||||
const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -246,11 +216,8 @@ class MonitorController {
|
||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
const monitorId = req.params.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -266,10 +233,7 @@ class MonitorController {
|
||||
|
||||
deleteAllMonitors = 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 deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
|
||||
|
||||
@@ -286,12 +250,8 @@ class MonitorController {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await editMonitorBodyValidation.validateAsync(req.body);
|
||||
const monitorId = req?.params?.monitorId;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
|
||||
|
||||
@@ -309,11 +269,8 @@ class MonitorController {
|
||||
try {
|
||||
await pauseMonitorParamValidation.validateAsync(req.params);
|
||||
|
||||
const monitorId = req.params.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -329,7 +286,8 @@ class MonitorController {
|
||||
|
||||
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { _id, teamId } = req.user;
|
||||
const _id = requireString(req?.user?._id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -365,10 +323,11 @@ class MonitorController {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
const teamId = req?.user?.teamId;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const type = parseMonitorTypeFilter(req.query?.type);
|
||||
const filter = optionalString(req.query?.filter, "filter");
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order });
|
||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -385,12 +344,9 @@ class MonitorController {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const explain = req?.query?.explain;
|
||||
const type = req?.query?.type;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||
const type = parseMonitorTypeFilter(req?.query?.type);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
|
||||
|
||||
@@ -406,14 +362,17 @@ class MonitorController {
|
||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
|
||||
|
||||
const explain = req?.query?.explain;
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
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 monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
||||
teamId,
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,12 +0,0 @@
|
||||
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
|
||||
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
|
||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||
throw new Error("Certificate not found");
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
|
||||
export { fetchMonitorCertificate };
|
||||
@@ -11,12 +11,9 @@ class MongoDB {
|
||||
inviteModule,
|
||||
statusPageModule,
|
||||
userModule,
|
||||
hardwareCheckModule,
|
||||
maintenanceWindowModule,
|
||||
monitorModule,
|
||||
networkCheckModule,
|
||||
notificationModule,
|
||||
pageSpeedCheckModule,
|
||||
recoveryModule,
|
||||
settingsModule,
|
||||
incidentModule,
|
||||
@@ -26,15 +23,12 @@ class MongoDB {
|
||||
this.userModule = userModule;
|
||||
this.inviteModule = inviteModule;
|
||||
this.recoveryModule = recoveryModule;
|
||||
this.pageSpeedCheckModule = pageSpeedCheckModule;
|
||||
this.hardwareCheckModule = hardwareCheckModule;
|
||||
this.checkModule = checkModule;
|
||||
this.maintenanceWindowModule = maintenanceWindowModule;
|
||||
this.monitorModule = monitorModule;
|
||||
this.notificationModule = notificationModule;
|
||||
this.settingsModule = settingsModule;
|
||||
this.statusPageModule = statusPageModule;
|
||||
this.networkCheckModule = networkCheckModule;
|
||||
this.incidentModule = incidentModule;
|
||||
}
|
||||
|
||||
@@ -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,95 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
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 };
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Schema, model, Types } from "mongoose";
|
||||
import { MonitorTypes, type MonitorType } from "@/types/monitor.js";
|
||||
import type {
|
||||
Check,
|
||||
CheckAudits,
|
||||
CheckCaptureInfo,
|
||||
CheckCpuInfo,
|
||||
CheckDiskInfo,
|
||||
CheckErrorInfo,
|
||||
CheckHostInfo,
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
CheckTimings,
|
||||
CheckTimingPhases,
|
||||
ILighthouseAudit,
|
||||
} from "@/types/check.js";
|
||||
|
||||
type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
teamId: Types.ObjectId;
|
||||
type: MonitorType;
|
||||
};
|
||||
|
||||
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
|
||||
metadata: CheckMetadataDocument;
|
||||
expiry: Date;
|
||||
ackAt?: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
interface CheckDocument extends CheckDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
const timingPhasesSchema = new Schema<CheckTimingPhases>(
|
||||
{
|
||||
wait: { type: Number, default: 0 },
|
||||
dns: { type: Number, default: 0 },
|
||||
tcp: { type: Number, default: 0 },
|
||||
tls: { type: Number, default: 0 },
|
||||
request: { type: Number, default: 0 },
|
||||
firstByte: { type: Number, default: 0 },
|
||||
download: { type: Number, default: 0 },
|
||||
total: { type: Number, default: 0 },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const timingsSchema = new Schema<CheckTimings>(
|
||||
{
|
||||
start: { type: Number, default: 0 },
|
||||
socket: { type: Number, default: 0 },
|
||||
lookup: { type: Number, default: 0 },
|
||||
connect: { type: Number, default: 0 },
|
||||
secureConnect: { type: Number, default: 0 },
|
||||
upload: { type: Number, default: 0 },
|
||||
response: { type: Number, default: 0 },
|
||||
end: { type: Number, default: 0 },
|
||||
phases: {
|
||||
type: timingPhasesSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const cpuSchema = new Schema<CheckCpuInfo>(
|
||||
{
|
||||
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 Schema<CheckMemoryInfo>(
|
||||
{
|
||||
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 Schema<CheckDiskInfo>(
|
||||
{
|
||||
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 Schema<CheckHostInfo>(
|
||||
{
|
||||
os: { type: String, default: "" },
|
||||
platform: { type: String, default: "" },
|
||||
kernel_version: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const errorSchema = new Schema<CheckErrorInfo>(
|
||||
{
|
||||
metric: { type: [String], default: [] },
|
||||
err: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const captureSchema = new Schema<CheckCaptureInfo>(
|
||||
{
|
||||
version: { type: String, default: "" },
|
||||
mode: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const networkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
|
||||
{
|
||||
name: { type: String, default: "" },
|
||||
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 lighthouseAuditSchema = new Schema<ILighthouseAudit>(
|
||||
{
|
||||
id: { type: String },
|
||||
title: { type: String },
|
||||
score: { type: Number },
|
||||
displayValue: { type: String },
|
||||
numericValue: { type: Number },
|
||||
numericUnit: { type: String },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const auditsSchema = new Schema<CheckAudits>(
|
||||
{
|
||||
cls: { type: lighthouseAuditSchema, default: undefined },
|
||||
si: { type: lighthouseAuditSchema, default: undefined },
|
||||
fcp: { type: lighthouseAuditSchema, default: undefined },
|
||||
lcp: { type: lighthouseAuditSchema, default: undefined },
|
||||
tbt: { type: lighthouseAuditSchema, default: undefined },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const metadataSchema = new Schema<CheckMetadataDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
required: true,
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
required: true,
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: MonitorTypes,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const CheckSchema = new Schema<CheckDocument>(
|
||||
{
|
||||
metadata: {
|
||||
type: metadataSchema,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
index: true,
|
||||
},
|
||||
responseTime: {
|
||||
type: Number,
|
||||
},
|
||||
timings: {
|
||||
type: timingsSchema,
|
||||
default: undefined,
|
||||
},
|
||||
statusCode: {
|
||||
type: Number,
|
||||
index: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
ack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ackAt: {
|
||||
type: Date,
|
||||
default: undefined,
|
||||
},
|
||||
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: () => [],
|
||||
},
|
||||
accessibility: {
|
||||
type: Number,
|
||||
},
|
||||
bestPractices: {
|
||||
type: Number,
|
||||
},
|
||||
seo: {
|
||||
type: Number,
|
||||
},
|
||||
performance: {
|
||||
type: Number,
|
||||
},
|
||||
audits: {
|
||||
type: auditsSchema,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
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.monitorId": 1, createdAt: -1 });
|
||||
CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 });
|
||||
CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
|
||||
|
||||
const CheckModel = model<CheckDocument>("Check", CheckSchema);
|
||||
|
||||
export type { CheckDocument, CheckMetadataDocument };
|
||||
export { CheckModel };
|
||||
export default CheckModel;
|
||||
@@ -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);
|
||||
Executable → Regular
+48
-24
@@ -1,18 +1,49 @@
|
||||
import mongoose from "mongoose";
|
||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
||||
import type { Monitor, MonitorMatchMethod, MonitorThresholds } from "@/types/monitor.js";
|
||||
import { MonitorTypes } from "@/types/monitor.js";
|
||||
import Check from "./Check.js";
|
||||
import MonitorStats from "./MonitorStats.js";
|
||||
import StatusPage from "./StatusPage.js";
|
||||
|
||||
const MonitorSchema = mongoose.Schema(
|
||||
type MonitorDocumentBase = Omit<
|
||||
Monitor,
|
||||
"id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "createdAt" | "updatedAt"
|
||||
> & {
|
||||
statusWindow: boolean[];
|
||||
notifications: Types.ObjectId[];
|
||||
selectedDisks: string[];
|
||||
matchMethod?: MonitorMatchMethod;
|
||||
thresholds?: MonitorThresholds;
|
||||
};
|
||||
|
||||
interface MonitorDocument extends MonitorDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
userId: Types.ObjectId;
|
||||
teamId: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const thresholdsSchema = new Schema<MonitorThresholds>(
|
||||
{
|
||||
usage_cpu: { type: Number },
|
||||
usage_memory: { type: Number },
|
||||
usage_disk: { type: Number },
|
||||
usage_temperature: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const MonitorSchema = new Schema<MonitorDocument>(
|
||||
{
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
required: true,
|
||||
@@ -43,7 +74,7 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"],
|
||||
enum: MonitorTypes,
|
||||
},
|
||||
ignoreTlsErrors: {
|
||||
type: Boolean,
|
||||
@@ -71,7 +102,6 @@ const MonitorSchema = mongoose.Schema(
|
||||
default: true,
|
||||
},
|
||||
interval: {
|
||||
// in milliseconds
|
||||
type: Number,
|
||||
default: 60000,
|
||||
},
|
||||
@@ -81,7 +111,7 @@ const MonitorSchema = mongoose.Schema(
|
||||
},
|
||||
notifications: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Notification",
|
||||
},
|
||||
],
|
||||
@@ -89,13 +119,7 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: String,
|
||||
},
|
||||
thresholds: {
|
||||
type: {
|
||||
usage_cpu: { type: Number },
|
||||
usage_memory: { type: Number },
|
||||
usage_disk: { type: Number },
|
||||
usage_temperature: { type: Number },
|
||||
},
|
||||
_id: false,
|
||||
type: thresholdsSchema,
|
||||
},
|
||||
alertThreshold: {
|
||||
type: Number,
|
||||
@@ -137,7 +161,7 @@ const MonitorSchema = mongoose.Schema(
|
||||
trim: true,
|
||||
maxLength: 50,
|
||||
default: null,
|
||||
set: function (value) {
|
||||
set(value: string | null) {
|
||||
return value && value.trim() ? value.trim() : null;
|
||||
},
|
||||
},
|
||||
@@ -148,7 +172,6 @@ const MonitorSchema = mongoose.Schema(
|
||||
);
|
||||
|
||||
MonitorSchema.pre("findOneAndDelete", async function (next) {
|
||||
// Delete checks and stats
|
||||
try {
|
||||
const doc = await this.model.findOne(this.getFilter());
|
||||
|
||||
@@ -157,20 +180,17 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
|
||||
}
|
||||
|
||||
await Check.deleteMany({ monitorId: doc._id });
|
||||
|
||||
// Deal with status pages
|
||||
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
|
||||
|
||||
await MonitorStats.deleteMany({ monitorId: doc?._id.toString() });
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
next(error as Error);
|
||||
}
|
||||
});
|
||||
|
||||
MonitorSchema.pre("deleteMany", async function (next) {
|
||||
const filter = this.getFilter();
|
||||
const monitors = await this.model.find(filter).select(["_id", "type"]).lean();
|
||||
const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[];
|
||||
|
||||
for (const monitor of monitors) {
|
||||
await Check.deleteMany({ monitorId: monitor._id });
|
||||
@@ -197,8 +217,8 @@ MonitorSchema.pre("save", function (next) {
|
||||
});
|
||||
|
||||
MonitorSchema.pre("findOneAndUpdate", function (next) {
|
||||
const update = this.getUpdate();
|
||||
if (update.alertThreshold) {
|
||||
const update = this.getUpdate() as UpdateQuery<MonitorDocument> | null;
|
||||
if (update && !Array.isArray(update) && update.alertThreshold !== undefined) {
|
||||
update.cpuAlertThreshold = update.alertThreshold;
|
||||
update.memoryAlertThreshold = update.alertThreshold;
|
||||
update.diskAlertThreshold = update.alertThreshold;
|
||||
@@ -209,4 +229,8 @@ MonitorSchema.pre("findOneAndUpdate", function (next) {
|
||||
|
||||
MonitorSchema.index({ teamId: 1, type: 1 });
|
||||
|
||||
export default mongoose.model("Monitor", MonitorSchema);
|
||||
const MonitorModel = model<MonitorDocument>("Monitor", MonitorSchema);
|
||||
|
||||
export type { MonitorDocument };
|
||||
export { MonitorModel };
|
||||
export default MonitorModel;
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { MonitorStats as MonitorStatsEntity } from "@/types/monitorStats.js";
|
||||
|
||||
type MonitorStatsDocumentBase = Omit<MonitorStatsEntity, "id" | "monitorId" | "createdAt" | "updatedAt"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
};
|
||||
|
||||
interface MonitorStatsDocument extends MonitorStatsDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStatsModel = model<MonitorStatsDocument>("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export type { MonitorStatsDocument };
|
||||
export { MonitorStatsModel };
|
||||
export default MonitorStatsModel;
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from "@/db/models/Monitor.js";
|
||||
export { default as MonitorModel } from "@/db/models/Monitor.js";
|
||||
|
||||
export * from "@/db/models/Check.js";
|
||||
export { default as CheckModel } from "@/db/models/Check.js";
|
||||
|
||||
export * from "@/db/models/MonitorStats.js";
|
||||
export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js";
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import mongoose from "mongoose";
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
|
||||
|
||||
const SERVICE_NAME = "checkModule";
|
||||
@@ -12,18 +14,15 @@ const dateRangeLookup = {
|
||||
};
|
||||
|
||||
class CheckModule {
|
||||
constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) {
|
||||
constructor({ logger, Monitor, User }) {
|
||||
this.logger = logger;
|
||||
this.Check = Check;
|
||||
this.HardwareCheck = HardwareCheck;
|
||||
this.PageSpeedCheck = PageSpeedCheck;
|
||||
this.Monitor = Monitor;
|
||||
this.User = User;
|
||||
}
|
||||
|
||||
createChecks = async (checks) => {
|
||||
try {
|
||||
await this.Check.insertMany(checks, { ordered: false });
|
||||
await CheckModel.insertMany(checks, { ordered: false, runValidators: true });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createCheck";
|
||||
@@ -41,7 +40,7 @@ class CheckModule {
|
||||
|
||||
// Match
|
||||
const matchStage = {
|
||||
monitorId: new ObjectId(monitorId),
|
||||
"metadata.monitorId": new ObjectId(monitorId),
|
||||
...(typeof status !== "undefined" && { status }),
|
||||
...(typeof ack !== "undefined" && ackStage),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
@@ -79,7 +78,7 @@ class CheckModule {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const checks = await this.Check.aggregate([
|
||||
const checks = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: sortOrder } },
|
||||
{
|
||||
@@ -115,7 +114,7 @@ class CheckModule {
|
||||
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
|
||||
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
"metadata.teamId": new ObjectId(teamId),
|
||||
status: false,
|
||||
...(typeof ack !== "undefined" && ackStage),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
@@ -170,7 +169,7 @@ class CheckModule {
|
||||
},
|
||||
];
|
||||
|
||||
const checks = await this.Check.aggregate(aggregatePipeline);
|
||||
const checks = await CheckModel.aggregate(aggregatePipeline);
|
||||
return checks[0];
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -181,7 +180,11 @@ class CheckModule {
|
||||
|
||||
ackCheck = async (checkId, teamId, ack) => {
|
||||
try {
|
||||
const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true });
|
||||
const updatedCheck = await CheckModel.findOneAndUpdate(
|
||||
{ _id: checkId, "metadata.teamId": teamId },
|
||||
{ $set: { ack, ackAt: new Date() } },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedCheck) {
|
||||
throw new Error("Check not found");
|
||||
@@ -197,7 +200,11 @@ class CheckModule {
|
||||
|
||||
ackAllChecks = async (monitorId, teamId, ack, path) => {
|
||||
try {
|
||||
const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } });
|
||||
const filter =
|
||||
path === "monitor"
|
||||
? { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }
|
||||
: { "metadata.teamId": new mongoose.Types.ObjectId(teamId) };
|
||||
const updatedChecks = await CheckModel.updateMany(filter, { $set: { ack, ackAt: new Date() } });
|
||||
return updatedChecks.modifiedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -209,9 +216,9 @@ class CheckModule {
|
||||
getChecksSummaryByTeamId = async ({ teamId }) => {
|
||||
try {
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
"metadata.teamId": new ObjectId(teamId),
|
||||
};
|
||||
const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
|
||||
const checks = await CheckModel.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
|
||||
return checks[0].summary;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -221,7 +228,7 @@ class CheckModule {
|
||||
};
|
||||
deleteChecks = async (monitorId) => {
|
||||
try {
|
||||
const result = await this.Check.deleteMany({ monitorId });
|
||||
const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId });
|
||||
return result.deletedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -237,7 +244,7 @@ class CheckModule {
|
||||
const monitorIds = teamMonitors.map((monitor) => monitor._id);
|
||||
|
||||
// Delete all checks for these monitors in one operation
|
||||
const deleteResult = await this.Check.deleteMany({ monitorId: { $in: monitorIds } });
|
||||
const deleteResult = await CheckModel.deleteMany({ "metadata.monitorId": { $in: monitorIds } });
|
||||
|
||||
return deleteResult.deletedCount;
|
||||
} catch (error) {
|
||||
@@ -249,7 +256,7 @@ class CheckModule {
|
||||
|
||||
updateChecksTTL = async (teamId, ttl) => {
|
||||
try {
|
||||
await this.Check.collection.dropIndex("expiry_1");
|
||||
await CheckModel.collection.dropIndex("expiry_1");
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
@@ -260,9 +267,9 @@ class CheckModule {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.Check.collection.createIndex(
|
||||
await CheckModel.collection.createIndex(
|
||||
{ expiry: 1 },
|
||||
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
|
||||
{ expireAfterSeconds: ttl, partialFilterExpression: { "metadata.mode": { $exists: true } } }
|
||||
);
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
+51
-104
@@ -1,20 +1,23 @@
|
||||
import {
|
||||
buildUptimeDetailsPipeline,
|
||||
buildHardwareDetailsPipeline,
|
||||
buildMonitorSummaryByTeamIdPipeline,
|
||||
buildMonitorsByTeamIdPipeline,
|
||||
buildMonitorsAndSummaryByTeamIdPipeline,
|
||||
buildMonitorsWithChecksByTeamIdPipeline,
|
||||
buildFilteredMonitorsByTeamIdPipeline,
|
||||
getHardwareStats,
|
||||
getUpChecks,
|
||||
getAggregateData,
|
||||
} from "./monitorModuleQueries.js";
|
||||
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
|
||||
const SERVICE_NAME = "monitorModule";
|
||||
|
||||
class MonitorModule {
|
||||
constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
|
||||
constructor({ Monitor, MonitorStats, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
|
||||
this.Monitor = Monitor;
|
||||
this.MonitorStats = MonitorStats;
|
||||
this.Check = Check;
|
||||
this.stringService = stringService;
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
@@ -123,15 +126,18 @@ class MonitorModule {
|
||||
|
||||
//Helper
|
||||
getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
|
||||
const objectId = new this.ObjectId(monitorId);
|
||||
const indexSpec = {
|
||||
monitorId: 1,
|
||||
updatedAt: sortOrder, // This will be 1 or -1
|
||||
"metadata.monitorId": 1,
|
||||
updatedAt: sortOrder,
|
||||
};
|
||||
|
||||
const matchBase = { "metadata.monitorId": objectId };
|
||||
|
||||
const [checksAll, checksForDateRange] = await Promise.all([
|
||||
this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
|
||||
this.Check.find({
|
||||
monitorId,
|
||||
CheckModel.find(matchBase).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
|
||||
CheckModel.find({
|
||||
...matchBase,
|
||||
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
|
||||
})
|
||||
.hint(indexSpec)
|
||||
@@ -188,17 +194,6 @@ class MonitorModule {
|
||||
};
|
||||
};
|
||||
|
||||
getAllMonitors = async () => {
|
||||
try {
|
||||
const monitors = await this.Monitor.find();
|
||||
return monitors;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getAllMonitors";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorById = async (monitorId) => {
|
||||
try {
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
@@ -238,7 +233,7 @@ class MonitorModule {
|
||||
|
||||
const dateString = formatLookup[dateRange];
|
||||
|
||||
const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
|
||||
const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
|
||||
|
||||
const monitorData = results[0];
|
||||
|
||||
@@ -258,48 +253,6 @@ class MonitorModule {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
try {
|
||||
// Get monitor, if we can't find it, abort with error
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
|
||||
}
|
||||
|
||||
// Get query params
|
||||
const sort = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get Checks for monitor in date range requested
|
||||
const dates = this.getDateRange(dateRange);
|
||||
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
|
||||
|
||||
// Build monitor stats
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
uptimeDuration: this.calculateUptimeDuration(checksAll),
|
||||
lastChecked: this.getLastChecked(checksAll),
|
||||
latestResponseTime: this.getLatestResponseTime(checksAll),
|
||||
periodIncidents: this.getIncidents(checksForDateRange),
|
||||
periodTotalChecks: checksForDateRange.length,
|
||||
checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize),
|
||||
};
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") {
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange);
|
||||
monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange);
|
||||
const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange);
|
||||
monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats);
|
||||
}
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorStatsById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ monitorId, dateRange }) => {
|
||||
try {
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
@@ -313,9 +266,17 @@ class MonitorModule {
|
||||
};
|
||||
const dateString = formatLookup[dateRange];
|
||||
|
||||
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
|
||||
const [aggregateData, upChecksCount, metrics] = await Promise.all([
|
||||
getAggregateData(monitorId, dates),
|
||||
getUpChecks(monitorId, dates),
|
||||
getHardwareStats(monitorId, dates, dateString),
|
||||
]);
|
||||
|
||||
const stats = hardwareStats[0];
|
||||
const stats = {
|
||||
aggregateData: aggregateData,
|
||||
upChecks: upChecksCount,
|
||||
checks: metrics,
|
||||
};
|
||||
|
||||
return {
|
||||
...monitor.toObject(),
|
||||
@@ -328,47 +289,33 @@ class MonitorModule {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => {
|
||||
limit = parseInt(limit);
|
||||
page = parseInt(page);
|
||||
rowsPerPage = parseInt(rowsPerPage);
|
||||
if (field === undefined) {
|
||||
field = "name";
|
||||
order = "asc";
|
||||
}
|
||||
// Build match stage
|
||||
const matchStage = { teamId: new this.ObjectId(teamId) };
|
||||
if (type !== undefined) {
|
||||
matchStage.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
|
||||
const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage }));
|
||||
const summary = summaryResult[0];
|
||||
|
||||
const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order }));
|
||||
|
||||
const filteredMonitors = await this.Monitor.aggregate(
|
||||
buildFilteredMonitorsByTeamIdPipeline({
|
||||
matchStage,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
field,
|
||||
order,
|
||||
limit,
|
||||
type,
|
||||
})
|
||||
);
|
||||
|
||||
const normalizedFilteredMonitors = filteredMonitors.map((monitor) => {
|
||||
if (!monitor.checks) {
|
||||
return monitor;
|
||||
getMonitorsByTeamId = async ({ teamId, type, filter }) => {
|
||||
try {
|
||||
const matchStage = { teamId: new this.ObjectId(teamId) };
|
||||
if (type !== undefined) {
|
||||
matchStage.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
monitor.checks = this.NormalizeData(monitor.checks, 10, 100);
|
||||
return monitor;
|
||||
});
|
||||
|
||||
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
|
||||
if (filter !== undefined && filter !== null && filter !== "") {
|
||||
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
|
||||
}
|
||||
const monitors = await this.Monitor.find(matchStage)
|
||||
.sort({ name: 1 })
|
||||
.select({
|
||||
_id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
url: 1,
|
||||
status: 1,
|
||||
isActive: 1,
|
||||
teamId: 1,
|
||||
})
|
||||
.lean();
|
||||
return monitors;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorsByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
|
||||
+740
@@ -0,0 +1,740 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
|
||||
const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
|
||||
return [
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": new ObjectId(monitorId),
|
||||
updatedAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
updatedAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
// For the response time chart, should return checks for date window
|
||||
// Grouped by: {day: hour}, {week: day}, {month: day}
|
||||
uptimePercentage: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
upChecks: {
|
||||
$sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
percentage: {
|
||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedAvgResponseTime: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedChecks: [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
// Up checks grouped by: {day: hour}, {week: day}, {month: day}
|
||||
groupedUpChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
// Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window
|
||||
groupedDownChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
let: { monitor_id: { $toObjectId: monitorId } },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$_id", "$$monitor_id"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
teamId: 1,
|
||||
name: 1,
|
||||
status: 1,
|
||||
interval: 1,
|
||||
type: 1,
|
||||
url: 1,
|
||||
isActive: 1,
|
||||
notifications: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
groupedAvgResponseTime: {
|
||||
$arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0],
|
||||
},
|
||||
|
||||
groupedChecks: "$groupedChecks",
|
||||
groupedUpChecks: "$groupedUpChecks",
|
||||
groupedDownChecks: "$groupedDownChecks",
|
||||
groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] },
|
||||
monitor: { $arrayElemAt: ["$monitor", 0] },
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildMonitorStatsPipeline = (monitor) => {
|
||||
return [
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
avgResponseTime: 1,
|
||||
uptimePercentage: 1,
|
||||
totalChecks: 1,
|
||||
timeSinceLastCheck: {
|
||||
$subtract: [Date.now(), "$lastCheckTimestamp"],
|
||||
},
|
||||
lastCheckTimestamp: 1,
|
||||
uptBurnt: { $toString: "$uptBurnt" },
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => {
|
||||
return [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalMonitors: { $sum: 1 },
|
||||
upMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", true] }, 1, 0],
|
||||
},
|
||||
},
|
||||
downMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", false] }, 1, 0],
|
||||
},
|
||||
},
|
||||
pausedMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => {
|
||||
const sort = { [field]: order === "asc" ? 1 : -1 };
|
||||
|
||||
return [
|
||||
{ $match: matchStage },
|
||||
{ $sort: sort },
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
port: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => {
|
||||
return [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$facet: {
|
||||
summary: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalMonitors: { $sum: 1 },
|
||||
upMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", true] }, 1, 0],
|
||||
},
|
||||
},
|
||||
downMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", false] }, 1, 0],
|
||||
},
|
||||
},
|
||||
pausedMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
monitors: [
|
||||
{ $sort: { name: 1 } },
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
summary: { $arrayElemAt: ["$summary", 0] },
|
||||
monitors: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
|
||||
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
|
||||
const sort = { [field]: order === "asc" ? 1 : -1 };
|
||||
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
|
||||
|
||||
// Match name
|
||||
if (typeof filter !== "undefined" && field === "name") {
|
||||
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
|
||||
}
|
||||
|
||||
// Match isActive
|
||||
if (typeof filter !== "undefined" && field === "isActive") {
|
||||
matchStage.isActive = filter === "true" ? true : false;
|
||||
}
|
||||
|
||||
if (typeof filter !== "undefined" && field === "status") {
|
||||
matchStage.status = filter === "true" ? true : false;
|
||||
}
|
||||
|
||||
// Match type
|
||||
if (typeof filter !== "undefined" && field === "type") {
|
||||
matchStage.type = filter;
|
||||
}
|
||||
|
||||
const monitorsPipeline = [
|
||||
{ $sort: sort },
|
||||
{ $skip: skip },
|
||||
...limitStage,
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
description: 1,
|
||||
type: 1,
|
||||
url: 1,
|
||||
isActive: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
uptimePercentage: 1,
|
||||
status: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Add checks
|
||||
if (limit) {
|
||||
const checksCollection = "checks";
|
||||
monitorsPipeline.push({
|
||||
$lookup: {
|
||||
from: checksCollection,
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { updatedAt: -1 } },
|
||||
{ $limit: limit },
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
status: 1,
|
||||
responseTime: 1,
|
||||
statusCode: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
originalResponseTime: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
as: "checks",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const pipeline = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$facet: {
|
||||
count: [{ $count: "monitorsCount" }],
|
||||
monitors: monitorsPipeline,
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
count: { $arrayElemAt: ["$count", 0] },
|
||||
monitors: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
return pipeline;
|
||||
};
|
||||
|
||||
const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
|
||||
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
|
||||
const sort = { [field]: order === "asc" ? 1 : -1 };
|
||||
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
|
||||
|
||||
if (typeof filter !== "undefined" && field === "name") {
|
||||
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
|
||||
}
|
||||
|
||||
if (typeof filter !== "undefined" && field === "status") {
|
||||
matchStage.status = filter === "true";
|
||||
}
|
||||
|
||||
const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage];
|
||||
|
||||
// Add checks
|
||||
if (limit) {
|
||||
const checksCollection = "checks";
|
||||
|
||||
pipeline.push({
|
||||
$lookup: {
|
||||
from: checksCollection,
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $limit: limit },
|
||||
],
|
||||
as: "checks",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
};
|
||||
|
||||
const buildGetMonitorsByTeamIdPipeline = (req) => {
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
|
||||
limit = parseInt(limit);
|
||||
page = parseInt(page);
|
||||
rowsPerPage = parseInt(rowsPerPage);
|
||||
if (field === undefined) {
|
||||
field = "name";
|
||||
order = "asc";
|
||||
}
|
||||
// Build the match stage
|
||||
const matchStage = { teamId: new ObjectId(req.params.teamId) };
|
||||
if (type !== undefined) {
|
||||
matchStage.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
|
||||
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
|
||||
const sort = { [field]: order === "asc" ? 1 : -1 };
|
||||
return [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$facet: {
|
||||
summary: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalMonitors: { $sum: 1 },
|
||||
upMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", true] }, 1, 0],
|
||||
},
|
||||
},
|
||||
downMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", false] }, 1, 0],
|
||||
},
|
||||
},
|
||||
pausedMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
monitors: [
|
||||
{ $sort: sort },
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
filteredMonitors: [
|
||||
...(filter !== undefined
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }],
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ $sort: sort },
|
||||
{ $skip: skip },
|
||||
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
|
||||
...(limit
|
||||
? [
|
||||
{
|
||||
$lookup: {
|
||||
from: "checks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
...(limit ? [{ $limit: limit }] : []),
|
||||
],
|
||||
as: "standardchecks",
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
$addFields: {
|
||||
checks: {
|
||||
$switch: {
|
||||
branches: [
|
||||
{
|
||||
case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] },
|
||||
then: "$standardchecks",
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
standardchecks: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
summary: { $arrayElemAt: ["$summary", 0] },
|
||||
filteredMonitors: 1,
|
||||
monitors: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export {
|
||||
buildUptimeDetailsPipeline,
|
||||
buildMonitorStatsPipeline,
|
||||
buildGetMonitorsByTeamIdPipeline,
|
||||
buildMonitorSummaryByTeamIdPipeline,
|
||||
buildMonitorsByTeamIdPipeline,
|
||||
buildMonitorsAndSummaryByTeamIdPipeline,
|
||||
buildMonitorsWithChecksByTeamIdPipeline,
|
||||
buildFilteredMonitorsByTeamIdPipeline,
|
||||
};
|
||||
|
||||
export const getAggregateData = async (monitorId, dates) => {
|
||||
const result = await CheckModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": new ObjectId(monitorId),
|
||||
"metadata.type": "hardware",
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
latestCheck: { $first: "$$ROOT" },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
return result[0] || { totalChecks: 0, latestCheck: null };
|
||||
};
|
||||
|
||||
export const getUpChecks = async (monitorId, dates) => {
|
||||
const count = await CheckModel.countDocuments({
|
||||
"metadata.monitorId": new ObjectId(monitorId),
|
||||
"metadata.type": "hardware",
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
status: true,
|
||||
});
|
||||
return { totalChecks: count };
|
||||
};
|
||||
|
||||
export const getHardwareStats = async (monitorId, dates, dateString) => {
|
||||
return await CheckModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": new ObjectId(monitorId),
|
||||
"metadata.type": "hardware",
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{
|
||||
$group: {
|
||||
_id: { $dateToString: { format: dateString, date: "$createdAt" } },
|
||||
avgCpuUsage: { $avg: "$cpu.usage_percent" },
|
||||
avgMemoryUsage: { $avg: "$memory.usage_percent" },
|
||||
avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } },
|
||||
disks: { $push: "$disk" },
|
||||
net: { $push: "$net" },
|
||||
updatedAts: { $push: "$updatedAt" },
|
||||
sampleDoc: { $first: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
avgCpuUsage: 1,
|
||||
avgMemoryUsage: 1,
|
||||
avgTemperature: {
|
||||
$map: {
|
||||
input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] },
|
||||
as: "idx",
|
||||
in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } },
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
$map: {
|
||||
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] },
|
||||
as: "dIdx",
|
||||
in: {
|
||||
name: { $concat: ["disk", { $toString: "$$dIdx" }] },
|
||||
readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } },
|
||||
writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } },
|
||||
totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } },
|
||||
freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } },
|
||||
usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
net: {
|
||||
$map: {
|
||||
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] },
|
||||
as: "nIdx",
|
||||
in: {
|
||||
name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] },
|
||||
bytesSentPerSecond: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaBytesRecv: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaPacketsSent: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaPacketsRecv: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaErrIn: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaErrOut: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaDropIn: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
deltaDropOut: {
|
||||
$let: {
|
||||
vars: {
|
||||
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
|
||||
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
|
||||
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
|
||||
},
|
||||
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
]);
|
||||
};
|
||||
+1
-1
@@ -153,7 +153,7 @@ class StatusPageModule {
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
$expr: { $eq: ["$metadata.monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
@@ -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 };
|
||||
@@ -1,7 +0,0 @@
|
||||
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
|
||||
|
||||
const runMigrations = async () => {
|
||||
await migrateStatusWindowThreshold();
|
||||
};
|
||||
|
||||
export { runMigrations };
|
||||
@@ -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);
|
||||
@@ -1,49 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const MonitorStatsSchema = new mongoose.Schema(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStats = mongoose.model("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export default MonitorStats;
|
||||
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Check, CheckAudits, MonitorType } from "@/types/index.js";
|
||||
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export interface PageSpeedChecksResult {
|
||||
monitorType: "pagespeed";
|
||||
checks: Check[];
|
||||
}
|
||||
|
||||
export interface HardwareChecksResult {
|
||||
monitorType: "hardware";
|
||||
aggregateData: {
|
||||
latestCheck: Check | null;
|
||||
totalChecks: number;
|
||||
};
|
||||
upChecks: {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
disks: Array<{
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
writeSpeed: number;
|
||||
totalBytes: number;
|
||||
freeBytes: number;
|
||||
usagePercent: number;
|
||||
}>;
|
||||
net: Array<{
|
||||
name: string;
|
||||
bytesSentPerSecond: number;
|
||||
deltaBytesRecv: number;
|
||||
deltaPacketsSent: number;
|
||||
deltaPacketsRecv: number;
|
||||
deltaErrIn: number;
|
||||
deltaErrOut: number;
|
||||
deltaDropIn: number;
|
||||
deltaDropOut: number;
|
||||
deltaFifoIn: number;
|
||||
deltaFifoOut: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UptimeChecksResult {
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
|
||||
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
|
||||
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
export interface IChecksRepository {
|
||||
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
|
||||
findDateRangeChecksByMonitor(
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string,
|
||||
options?: { type?: MonitorType }
|
||||
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import { IChecksRepository } from "@/repositories/index.js";
|
||||
import type {
|
||||
Check,
|
||||
CheckAudits,
|
||||
CheckCaptureInfo,
|
||||
CheckCpuInfo,
|
||||
CheckDiskInfo,
|
||||
CheckErrorInfo,
|
||||
CheckHostInfo,
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
CheckTimings,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import {
|
||||
getAggregateData as getHardwareAggregateData,
|
||||
getHardwareStats,
|
||||
getUpChecks as getHardwareUpChecks,
|
||||
} from "@/db/modules/monitorModuleQueries.js";
|
||||
|
||||
export type LatestChecksMap = Record<string, Check[]>;
|
||||
|
||||
class MongoChecksRepistory implements IChecksRepository {
|
||||
private toEntity = (doc: CheckDocument): Check => {
|
||||
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
|
||||
};
|
||||
|
||||
const toDateString = (value?: Date | string | null): string => {
|
||||
if (!value) {
|
||||
return new Date(0).toISOString();
|
||||
}
|
||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||
};
|
||||
|
||||
const toOptionalDateString = (value?: Date | string | null): string | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return toDateString(value);
|
||||
};
|
||||
|
||||
const mapTimings = (timings?: CheckTimings): CheckTimings => {
|
||||
const phases = timings?.phases ?? {
|
||||
wait: 0,
|
||||
dns: 0,
|
||||
tcp: 0,
|
||||
tls: 0,
|
||||
request: 0,
|
||||
firstByte: 0,
|
||||
download: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
start: timings?.start ?? 0,
|
||||
socket: timings?.socket ?? 0,
|
||||
lookup: timings?.lookup ?? 0,
|
||||
connect: timings?.connect ?? 0,
|
||||
secureConnect: timings?.secureConnect ?? 0,
|
||||
upload: timings?.upload ?? 0,
|
||||
response: timings?.response ?? 0,
|
||||
end: timings?.end ?? 0,
|
||||
phases,
|
||||
};
|
||||
};
|
||||
|
||||
const mapCpu = (cpu?: CheckCpuInfo): CheckCpuInfo => ({
|
||||
physical_core: cpu?.physical_core ?? 0,
|
||||
logical_core: cpu?.logical_core ?? 0,
|
||||
frequency: cpu?.frequency ?? 0,
|
||||
temperature: cpu?.temperature ?? [],
|
||||
free_percent: cpu?.free_percent ?? 0,
|
||||
usage_percent: cpu?.usage_percent ?? 0,
|
||||
});
|
||||
|
||||
const mapMemory = (memory?: CheckMemoryInfo): CheckMemoryInfo => ({
|
||||
total_bytes: memory?.total_bytes ?? 0,
|
||||
available_bytes: memory?.available_bytes ?? 0,
|
||||
used_bytes: memory?.used_bytes ?? 0,
|
||||
usage_percent: memory?.usage_percent ?? 0,
|
||||
});
|
||||
|
||||
const mapHost = (host?: CheckHostInfo): CheckHostInfo => ({
|
||||
os: host?.os ?? "",
|
||||
platform: host?.platform ?? "",
|
||||
kernel_version: host?.kernel_version ?? "",
|
||||
});
|
||||
|
||||
const mapCapture = (capture?: CheckCaptureInfo): CheckCaptureInfo => ({
|
||||
version: capture?.version ?? "",
|
||||
mode: capture?.mode ?? "",
|
||||
});
|
||||
|
||||
const mapDisks = (disks?: CheckDiskInfo[]): CheckDiskInfo[] =>
|
||||
(disks ?? []).map((disk) => ({
|
||||
device: disk?.device ?? "",
|
||||
mountpoint: disk?.mountpoint ?? "",
|
||||
read_speed_bytes: disk?.read_speed_bytes ?? 0,
|
||||
write_speed_bytes: disk?.write_speed_bytes ?? 0,
|
||||
total_bytes: disk?.total_bytes ?? 0,
|
||||
free_bytes: disk?.free_bytes ?? 0,
|
||||
usage_percent: disk?.usage_percent ?? 0,
|
||||
}));
|
||||
|
||||
const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] =>
|
||||
(errors ?? []).map((error) => ({
|
||||
metric: error?.metric ?? [],
|
||||
err: error?.err ?? "",
|
||||
}));
|
||||
|
||||
const mapNet = (net?: CheckNetworkInterfaceInfo[]): CheckNetworkInterfaceInfo[] =>
|
||||
(net ?? []).map((iface) => ({
|
||||
name: iface?.name ?? "",
|
||||
bytes_sent: iface?.bytes_sent ?? 0,
|
||||
bytes_recv: iface?.bytes_recv ?? 0,
|
||||
packets_sent: iface?.packets_sent ?? 0,
|
||||
packets_recv: iface?.packets_recv ?? 0,
|
||||
err_in: iface?.err_in ?? 0,
|
||||
err_out: iface?.err_out ?? 0,
|
||||
drop_in: iface?.drop_in ?? 0,
|
||||
drop_out: iface?.drop_out ?? 0,
|
||||
fifo_in: iface?.fifo_in ?? 0,
|
||||
fifo_out: iface?.fifo_out ?? 0,
|
||||
}));
|
||||
|
||||
const mapAudits = (audits?: CheckAudits): CheckAudits | undefined => {
|
||||
if (!audits) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
cls: audits.cls ?? 0,
|
||||
si: audits.si ?? 0,
|
||||
fcp: audits.fcp ?? 0,
|
||||
lcp: audits.lcp ?? 0,
|
||||
tbt: audits.tbt ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({
|
||||
monitorId: toStringId(metadata.monitorId),
|
||||
teamId: toStringId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
});
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
metadata: mapMetadata(doc.metadata),
|
||||
status: doc.status ?? false,
|
||||
responseTime: doc.responseTime ?? 0,
|
||||
timings: mapTimings(doc.timings),
|
||||
statusCode: doc.statusCode ?? 0,
|
||||
message: doc.message ?? "",
|
||||
ack: doc.ack ?? false,
|
||||
ackAt: toOptionalDateString(doc.ackAt),
|
||||
expiry: toDateString(doc.expiry),
|
||||
cpu: mapCpu(doc.cpu),
|
||||
memory: mapMemory(doc.memory),
|
||||
disk: mapDisks(doc.disk),
|
||||
host: mapHost(doc.host),
|
||||
errors: mapErrors(doc.errors),
|
||||
capture: mapCapture(doc.capture),
|
||||
net: mapNet(doc.net),
|
||||
accessibility: doc.accessibility,
|
||||
bestPractices: doc.bestPractices,
|
||||
seo: doc.seo,
|
||||
performance: doc.performance,
|
||||
audits: mapAudits(doc.audits),
|
||||
__v: doc.__v ?? 0,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
|
||||
if (monitorIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const limitPerMonitor = options?.limitPerMonitor ?? 25;
|
||||
const maxIntervalMs = Number(10 * 60 * 1000);
|
||||
const bufferMs = Number(maxIntervalMs);
|
||||
const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs;
|
||||
|
||||
const cutoffDate = new Date(Date.now() - lookbackMs);
|
||||
const checkGroups = await CheckModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": { $in: mongoIds },
|
||||
createdAt: { $gte: cutoffDate },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$metadata.monitorId",
|
||||
latestChecks: {
|
||||
$topN: {
|
||||
n: limitPerMonitor,
|
||||
sortBy: { createdAt: -1 },
|
||||
output: "$$ROOT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return checkGroups.reduce<LatestChecksMap>((acc, group) => {
|
||||
const monitorId = group._id.toString();
|
||||
acc[monitorId] = (group.latestChecks ?? []).map((doc: CheckDocument) => this.toEntity(doc));
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
|
||||
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
|
||||
if (options?.type === "hardware") {
|
||||
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||
}
|
||||
if (options?.type === "pagespeed") {
|
||||
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
|
||||
}
|
||||
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
|
||||
};
|
||||
|
||||
private findUptimeDateRangeChecks = async (
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
||||
monitorObjectId: mongoose.Types.ObjectId,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
) => {
|
||||
const matchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
updatedAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
const [result] = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { updatedAt: 1 } },
|
||||
{
|
||||
$facet: {
|
||||
uptimePercentage: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
percentage: {
|
||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedAvgResponseTime: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedChecks: [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
],
|
||||
groupedUpChecks: [
|
||||
{ $match: { status: true } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
],
|
||||
groupedDownChecks: [
|
||||
{ $match: { status: false } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
|
||||
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
|
||||
|
||||
return {
|
||||
monitorType,
|
||||
groupedChecks: result?.groupedChecks ?? [],
|
||||
groupedUpChecks: result?.groupedUpChecks ?? [],
|
||||
groupedDownChecks: result?.groupedDownChecks ?? [],
|
||||
uptimePercentage,
|
||||
avgResponseTime,
|
||||
};
|
||||
};
|
||||
|
||||
private findHardwareDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
|
||||
const monitorId = monitorObjectId.toHexString();
|
||||
const dates = { start: startDate, end: endDate };
|
||||
const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([
|
||||
getHardwareAggregateData(monitorId, dates),
|
||||
getHardwareUpChecks(monitorId, dates),
|
||||
getHardwareStats(monitorId, dates, dateString),
|
||||
]);
|
||||
|
||||
const aggregateData = {
|
||||
latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null,
|
||||
totalChecks: aggregateDataDoc?.totalChecks ?? 0,
|
||||
};
|
||||
|
||||
const upChecks = {
|
||||
totalChecks: upChecksDoc?.totalChecks ?? 0,
|
||||
};
|
||||
|
||||
const checks = (hardwareMetrics ?? []).map((metric) => ({
|
||||
_id: metric._id,
|
||||
avgCpuUsage: metric.avgCpuUsage ?? 0,
|
||||
avgMemoryUsage: metric.avgMemoryUsage ?? 0,
|
||||
avgTemperature: metric.avgTemperature ?? [],
|
||||
disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({
|
||||
name: disk?.name ?? "",
|
||||
readSpeed: disk?.readSpeed ?? 0,
|
||||
writeSpeed: disk?.writeSpeed ?? 0,
|
||||
totalBytes: disk?.totalBytes ?? 0,
|
||||
freeBytes: disk?.freeBytes ?? 0,
|
||||
usagePercent: disk?.usagePercent ?? 0,
|
||||
})),
|
||||
net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({
|
||||
name: iface?.name ?? "",
|
||||
bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0,
|
||||
deltaBytesRecv: iface?.deltaBytesRecv ?? 0,
|
||||
deltaPacketsSent: iface?.deltaPacketsSent ?? 0,
|
||||
deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0,
|
||||
deltaErrIn: iface?.deltaErrIn ?? 0,
|
||||
deltaErrOut: iface?.deltaErrOut ?? 0,
|
||||
deltaDropIn: iface?.deltaDropIn ?? 0,
|
||||
deltaDropOut: iface?.deltaDropOut ?? 0,
|
||||
deltaFifoIn: iface?.deltaFifoIn ?? 0,
|
||||
deltaFifoOut: iface?.deltaFifoOut ?? 0,
|
||||
})),
|
||||
}));
|
||||
|
||||
return {
|
||||
monitorType: "hardware" as const,
|
||||
aggregateData,
|
||||
upChecks,
|
||||
checks,
|
||||
};
|
||||
};
|
||||
|
||||
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
|
||||
const matchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
|
||||
const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
|
||||
return {
|
||||
monitorType: "pagespeed" as const,
|
||||
checks: checks.map((doc) => this.toEntity(doc)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoChecksRepistory;
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from "@/repositories/monitors/IMonitorsRepository.js";
|
||||
export { default as MongoMonitorsRepository } from "@/repositories/monitors/MongoMonitorsRepository.js";
|
||||
|
||||
export * from "@/repositories/checks/IChecksRepository.js";
|
||||
export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js";
|
||||
export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js";
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
export interface IMonitorStatsRepository {
|
||||
// create
|
||||
// single fetch
|
||||
findByMonitorId(monitorId: string): Promise<MonitorStats>;
|
||||
// update
|
||||
// delete
|
||||
// other
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type MonitorStatsDocument, MonitorStatsModel } from "@/db/models/index.js";
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
import { IMonitorStatsRepository } from "@/repositories/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
private toEntity = (doc: MonitorStatsDocument): MonitorStats => {
|
||||
const toStringId = (value: unknown): string => {
|
||||
if (value instanceof mongoose.Types.ObjectId) {
|
||||
return value.toString();
|
||||
}
|
||||
return value?.toString() ?? "";
|
||||
};
|
||||
|
||||
const toDateString = (value: Date | string): string => {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
};
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
monitorId: toStringId(doc.monitorId),
|
||||
avgResponseTime: doc.avgResponseTime,
|
||||
totalChecks: doc.totalChecks,
|
||||
totalUpChecks: doc.totalUpChecks,
|
||||
totalDownChecks: doc.totalDownChecks,
|
||||
uptimePercentage: doc.uptimePercentage,
|
||||
lastCheckTimestamp: doc.lastCheckTimestamp,
|
||||
lastResponseTime: doc.lastResponseTime,
|
||||
timeOfLastFailure: doc.timeOfLastFailure,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
findByMonitorId = async (monitorId: string): Promise<MonitorStats> => {
|
||||
const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
if (!monitorStats) {
|
||||
throw new AppError({ message: "Monitor stats not found", status: 404 });
|
||||
}
|
||||
return this.toEntity(monitorStats);
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorStatsRepository;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { type MonitorType, type Monitor } from "@/types/index.js";
|
||||
|
||||
export interface TeamQueryConfig {
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface IMonitorsRepository {
|
||||
// create
|
||||
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
|
||||
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
|
||||
// single fetch
|
||||
findById(monitorId: string, teamId?: string): Promise<Monitor | null>;
|
||||
|
||||
// collection fetch
|
||||
findAll(): Promise<Monitor[] | null>;
|
||||
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
|
||||
// update
|
||||
update(monitorId: string, updates: Partial<Monitor>): Promise<Monitor>;
|
||||
// delete
|
||||
deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>;
|
||||
|
||||
// counts
|
||||
findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { MonitorModel } from "@/db/models/index.js";
|
||||
import type { MonitorDocument } from "@/db/models/Monitor.js";
|
||||
import type { Monitor, MonitorType } from "@/types/monitor.js";
|
||||
import mongoose, { type FilterQuery } from "mongoose";
|
||||
import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
create = async (monitor: Monitor, teamId: string, userId: string) => {
|
||||
const monitorModel = new MonitorModel({ ...monitor, teamId, userId });
|
||||
const saved = await monitorModel.save();
|
||||
return this.toEntity(saved);
|
||||
};
|
||||
|
||||
createBulkMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
|
||||
if (!monitors.length) {
|
||||
return [];
|
||||
}
|
||||
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
|
||||
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
||||
return this.mapDocuments(inserted);
|
||||
};
|
||||
|
||||
findById = async (monitorId: string, teamId?: string): Promise<Monitor> => {
|
||||
const match: { _id: string; teamId?: string } = { _id: monitorId };
|
||||
if (teamId) {
|
||||
match.teamId = teamId;
|
||||
}
|
||||
const monitor = await MonitorModel.findOne(match);
|
||||
if (!monitor) {
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new AppError({
|
||||
message: `Monitor with ID ${monitorId} not found.`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.toEntity(monitor);
|
||||
};
|
||||
|
||||
findAll = async (): Promise<Monitor[]> => {
|
||||
const monitors = await MonitorModel.find();
|
||||
return this.mapDocuments(monitors);
|
||||
};
|
||||
|
||||
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
|
||||
const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
|
||||
|
||||
const query: Record<string, unknown> = {
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
};
|
||||
|
||||
if (type !== undefined) {
|
||||
query.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
|
||||
if (filter !== undefined) {
|
||||
switch (field) {
|
||||
case "name":
|
||||
query.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
|
||||
break;
|
||||
case "isActive":
|
||||
query.isActive = filter === "true";
|
||||
break;
|
||||
case "status":
|
||||
query.status = filter === "true";
|
||||
break;
|
||||
case "type":
|
||||
query.type = filter;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const sort = { [field]: order === "asc" ? 1 : -1 } as const;
|
||||
const skip = Math.max(page, 0) * rowsPerPage;
|
||||
const limitValue = limit ?? rowsPerPage;
|
||||
|
||||
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec();
|
||||
|
||||
return this.mapDocuments(documents);
|
||||
};
|
||||
|
||||
findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise<number> => {
|
||||
const { type } = config ?? {};
|
||||
|
||||
const query: FilterQuery<MonitorDocument> = {
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
};
|
||||
|
||||
if (type !== undefined) {
|
||||
query.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
|
||||
const count = await MonitorModel.countDocuments(query);
|
||||
return count;
|
||||
};
|
||||
|
||||
update = async (monitorId: string, patch: Partial<Monitor>) => {
|
||||
const updatedMonitor = await MonitorModel.findOneAndUpdate(
|
||||
{ _id: monitorId },
|
||||
{
|
||||
$set: {
|
||||
...patch,
|
||||
},
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
if (!updatedMonitor) {
|
||||
throw new AppError({ message: `Failed to update monitor with id ${monitorId}`, status: 500 });
|
||||
}
|
||||
return this.toEntity(updatedMonitor);
|
||||
};
|
||||
|
||||
deleteByTeamId = async (teamId: string) => {
|
||||
const monitors = await MonitorModel.find({ teamId });
|
||||
const { deletedCount } = await MonitorModel.deleteMany({ teamId });
|
||||
|
||||
return { monitors: this.mapDocuments(monitors), deletedCount };
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
|
||||
if (!documents?.length) {
|
||||
return [];
|
||||
}
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
private toEntity = (doc: MonitorDocument): Monitor => {
|
||||
const toStringId = (value: unknown): string => {
|
||||
if (value instanceof mongoose.Types.ObjectId) {
|
||||
return value.toString();
|
||||
}
|
||||
return value?.toString() ?? "";
|
||||
};
|
||||
|
||||
const toDateString = (value: Date | string): string => {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
};
|
||||
|
||||
const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification));
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
userId: toStringId(doc.userId),
|
||||
teamId: toStringId(doc.teamId),
|
||||
name: doc.name,
|
||||
description: doc.description ?? undefined,
|
||||
status: doc.status ?? undefined,
|
||||
statusWindow: doc.statusWindow ?? [],
|
||||
statusWindowSize: doc.statusWindowSize,
|
||||
statusWindowThreshold: doc.statusWindowThreshold,
|
||||
type: doc.type,
|
||||
ignoreTlsErrors: doc.ignoreTlsErrors,
|
||||
jsonPath: doc.jsonPath ?? undefined,
|
||||
expectedValue: doc.expectedValue ?? undefined,
|
||||
matchMethod: doc.matchMethod ?? undefined,
|
||||
url: doc.url,
|
||||
port: doc.port ?? undefined,
|
||||
isActive: doc.isActive,
|
||||
interval: doc.interval,
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
secret: doc.secret ?? undefined,
|
||||
thresholds: doc.thresholds ?? undefined,
|
||||
alertThreshold: doc.alertThreshold,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
memoryAlertThreshold: doc.memoryAlertThreshold,
|
||||
diskAlertThreshold: doc.diskAlertThreshold,
|
||||
tempAlertThreshold: doc.tempAlertThreshold,
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorsRepository;
|
||||
@@ -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,13 +1,15 @@
|
||||
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
|
||||
});
|
||||
|
||||
class MonitorRoutes {
|
||||
constructor(monitorController) {
|
||||
private router: Router;
|
||||
private monitorController: any;
|
||||
constructor(monitorController: any) {
|
||||
this.router = Router();
|
||||
this.monitorController = monitorController;
|
||||
this.initRoutes();
|
||||
@@ -25,10 +27,11 @@ class MonitorRoutes {
|
||||
|
||||
// Hardware routes
|
||||
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
|
||||
// PageSpeed routes
|
||||
this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
|
||||
|
||||
// General monitor routes
|
||||
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
|
||||
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
|
||||
|
||||
// Util routes
|
||||
this.router.get("/certificate/:monitorId", (req, res, next) => {
|
||||
@@ -36,7 +39,6 @@ class MonitorRoutes {
|
||||
});
|
||||
|
||||
// General monitor CRUD routes
|
||||
this.router.get("/", this.monitorController.getAllMonitors);
|
||||
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
|
||||
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
import { createMonitorsBodyValidation } from "@/validation/joi.js";
|
||||
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
|
||||
import { type Monitor } from "@/types/index.js";
|
||||
import type { MonitorType } from "@/types/monitor.js";
|
||||
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
import { AppError } from "../infrastructure/errorService.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
|
||||
|
||||
export interface IMonitorService {
|
||||
readonly serviceName: string;
|
||||
verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise<void>;
|
||||
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
||||
createBulkMonitors(fileData: string, userId: string, teamId: string): Promise<any>;
|
||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<any[]>;
|
||||
|
||||
// read
|
||||
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
|
||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
||||
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||
getMonitorsByTeamId(args: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
}): Promise<any>;
|
||||
getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise<any>;
|
||||
getMonitorsWithChecksByTeamId(args: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
explain?: boolean;
|
||||
}): Promise<{ count: number; monitors: any[] }>;
|
||||
getAllGames(): any;
|
||||
getGroupsByTeamId(args: { teamId: string }): Promise<any[]>;
|
||||
|
||||
// update
|
||||
editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise<void>;
|
||||
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<any>;
|
||||
|
||||
// delete
|
||||
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<any>;
|
||||
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
||||
|
||||
// other
|
||||
sendTestEmail(args: { to: string }): Promise<string>;
|
||||
exportMonitorsToCSV(args: { teamId: string }): Promise<string>;
|
||||
exportMonitorsToJSON(args: { teamId: string }): Promise<any[]>;
|
||||
}
|
||||
|
||||
export class MonitorService implements IMonitorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private db: any;
|
||||
private jobQueue: any;
|
||||
private stringService: any;
|
||||
private emailService: any;
|
||||
private papaparse: any;
|
||||
private logger: any;
|
||||
private errorService: any;
|
||||
private games: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private checksRepository: IChecksRepository;
|
||||
private monitorStatsRepository: IMonitorStatsRepository;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
jobQueue,
|
||||
stringService,
|
||||
emailService,
|
||||
papaparse,
|
||||
logger,
|
||||
errorService,
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
}: {
|
||||
db: any;
|
||||
jobQueue: any;
|
||||
stringService: any;
|
||||
emailService: any;
|
||||
papaparse: any;
|
||||
logger: any;
|
||||
errorService: any;
|
||||
games: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
this.papaparse = papaparse;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
this.games = games;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.checksRepository = checksRepository;
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
return MonitorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private getDateRange = (dateRange: DateRangeKey) => {
|
||||
const startDates = {
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
all: new Date(0),
|
||||
};
|
||||
return {
|
||||
start: startDates[dateRange],
|
||||
end: new Date(),
|
||||
};
|
||||
};
|
||||
|
||||
private getDateFormat = (dateRange: DateRangeKey): string => {
|
||||
const formatLookup = {
|
||||
recent: "%Y-%m-%dT%H:%M:00Z",
|
||||
day: "%Y-%m-%dT%H:00:00Z",
|
||||
week: "%Y-%m-%dT00:00:00Z",
|
||||
month: "%Y-%m-%dT00:00:00Z",
|
||||
all: "%Y-%m-%dT00:00:00Z",
|
||||
};
|
||||
return formatLookup[dateRange];
|
||||
};
|
||||
|
||||
verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<void> => {
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
if (!monitor?.teamId?.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
};
|
||||
|
||||
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => {
|
||||
const monitor = await this.monitorsRepository.create(body, teamId, userId);
|
||||
if (!monitor) {
|
||||
throw new AppError("Failed to create monitor", 500);
|
||||
}
|
||||
|
||||
this.jobQueue.addJob(monitor.id, monitor);
|
||||
};
|
||||
|
||||
createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise<any> => {
|
||||
const { parse } = this.papaparse;
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value: string, header: string): string | number | undefined => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise<void> => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw this.errorService.createServerError("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw this.errorService.createServerError("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor: Monitor) => ({
|
||||
...monitor,
|
||||
userId,
|
||||
teamId,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor: Monitor) => {
|
||||
this.jobQueue.addJob(monitor.id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
resolve(monitors);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json");
|
||||
|
||||
const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8"));
|
||||
const monitors: Monitor[] = demoData.map((monitor: Monitor) => {
|
||||
return {
|
||||
userId,
|
||||
teamId,
|
||||
name: monitor.name,
|
||||
description: monitor.name,
|
||||
type: "http",
|
||||
url: monitor.url,
|
||||
interval: 60000,
|
||||
};
|
||||
});
|
||||
const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
|
||||
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
||||
return demoMonitors;
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async ({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
normalize,
|
||||
}: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
dateRange: string;
|
||||
normalize?: boolean;
|
||||
}): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
||||
|
||||
if (
|
||||
checksData.monitorType !== "http" &&
|
||||
checksData.monitorType !== "ping" &&
|
||||
checksData.monitorType !== "docker" &&
|
||||
checksData.monitorType !== "port" &&
|
||||
checksData.monitorType !== "game"
|
||||
) {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||
}
|
||||
|
||||
return {
|
||||
monitorData: {
|
||||
monitor,
|
||||
groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100),
|
||||
groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100),
|
||||
groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100),
|
||||
groupedAvgResponseTime: checksData.avgResponseTime,
|
||||
groupedUptimePercentage: checksData.uptimePercentage,
|
||||
},
|
||||
monitorStats,
|
||||
};
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
if (monitor.type !== "hardware") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 });
|
||||
}
|
||||
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
|
||||
if (checksData.monitorType !== "hardware") {
|
||||
throw new AppError({ message: "Unable to load hardware stats for this monitor", status: 500 });
|
||||
}
|
||||
|
||||
const stats = {
|
||||
aggregateData: checksData.aggregateData,
|
||||
upChecks: checksData.upChecks,
|
||||
checks: checksData.checks,
|
||||
};
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
stats,
|
||||
};
|
||||
};
|
||||
|
||||
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
if (monitor.type !== "pagespeed") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 });
|
||||
}
|
||||
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
|
||||
if (checksData.monitorType !== "pagespeed") {
|
||||
throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 });
|
||||
}
|
||||
return {
|
||||
...monitor,
|
||||
checks: checksData.checks,
|
||||
};
|
||||
};
|
||||
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise<any> => {
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
||||
type,
|
||||
filter,
|
||||
});
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({
|
||||
teamId,
|
||||
type,
|
||||
explain,
|
||||
}: {
|
||||
teamId: string;
|
||||
type?: string | string[];
|
||||
explain?: boolean;
|
||||
}): Promise<any> => {
|
||||
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
|
||||
type,
|
||||
explain,
|
||||
teamId,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
getMonitorsWithChecksByTeamId = async ({
|
||||
teamId,
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
explain,
|
||||
}: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
explain?: boolean;
|
||||
}): Promise<{ count: number; monitors: any[] }> => {
|
||||
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
});
|
||||
|
||||
const monitorsList = (monitors ?? []) as Monitor[];
|
||||
const snapshotTypes: MonitorType[] = ["hardware", "pagespeed"];
|
||||
const requestedTypes = Array.isArray(type) ? type : type ? [type] : [];
|
||||
const snapshotOnlyRequest =
|
||||
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
|
||||
|
||||
const limitPerMonitor = snapshotOnlyRequest ? 1 : 25;
|
||||
const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(
|
||||
monitorsList.map((monitor) => monitor.id),
|
||||
{ limitPerMonitor }
|
||||
);
|
||||
|
||||
const monitorsWithChecks = monitorsList.map((monitor: Monitor) => {
|
||||
const rawChecks = checksMap[monitor.id] ?? [];
|
||||
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
|
||||
const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100);
|
||||
return {
|
||||
...monitor,
|
||||
checks,
|
||||
};
|
||||
});
|
||||
|
||||
return { count, monitors: monitorsWithChecks };
|
||||
};
|
||||
|
||||
getAllGames = (): any => {
|
||||
return this.games;
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||
const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId });
|
||||
return groups;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: any }): Promise<void> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body });
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
};
|
||||
|
||||
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteAllMonitors = async ({ teamId }: { teamId: string }): Promise<number> => {
|
||||
const { monitors, deletedCount } = await this.monitorsRepository.deleteByTeamId(teamId);
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
try {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.checkModule.deleteChecks(monitor.id);
|
||||
await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id);
|
||||
await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id);
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
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 this.errorService.createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise<string> => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
type: monitor.type,
|
||||
url: monitor.url,
|
||||
interval: monitor.interval,
|
||||
port: monitor.port,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
isActive: monitor.isActive,
|
||||
}));
|
||||
|
||||
const csv = this.papaparse.unparse(csvData);
|
||||
return csv;
|
||||
};
|
||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const json = monitors?.filteredMonitors
|
||||
?.map((monitor: any) => {
|
||||
const initialType = monitor.type;
|
||||
let parsedType;
|
||||
|
||||
if (initialType === "hardware") {
|
||||
parsedType = "infrastructure";
|
||||
} else if (initialType === "http") {
|
||||
if (monitor.url.startsWith("https://")) {
|
||||
parsedType = "https";
|
||||
} else {
|
||||
parsedType = "http";
|
||||
}
|
||||
} else if (initialType === "pagespeed") {
|
||||
parsedType = initialType;
|
||||
} else {
|
||||
// Skip unsupported types
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
type: parsedType,
|
||||
interval: monitor.interval,
|
||||
n: monitor.statusWindowSize,
|
||||
secret: monitor.secret,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return json;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@/service/business/monitorService.js";
|
||||
+14
-13
@@ -4,19 +4,20 @@ const SERVICE_NAME = "JobQueue";
|
||||
class SuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ envSettings, db, logger, helper }) {
|
||||
constructor({ envSettings, db, logger, helper, monitorsRepository }) {
|
||||
this.envSettings = envSettings;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ envSettings, db, logger, helper }) {
|
||||
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper });
|
||||
static async create({ envSettings, db, logger, helper, monitorsRepository }) {
|
||||
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper, monitorsRepository });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
@@ -33,11 +34,11 @@ class SuperSimpleQueue {
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
const monitors = await this.db.monitorModule.getAllMonitors();
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
for (const monitor of monitors) {
|
||||
const randomOffset = Math.floor(Math.random() * 100);
|
||||
setTimeout(() => {
|
||||
this.addJob(monitor._id, monitor);
|
||||
this.addJob(monitor.id, monitor);
|
||||
}, randomOffset);
|
||||
}
|
||||
|
||||
@@ -55,44 +56,44 @@ class SuperSimpleQueue {
|
||||
|
||||
addJob = async (monitorId, monitor) => {
|
||||
this.scheduler.addJob({
|
||||
id: monitorId.toString(),
|
||||
id: monitorId,
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
active: monitor.isActive,
|
||||
data: monitor.toObject(),
|
||||
data: monitor,
|
||||
});
|
||||
};
|
||||
|
||||
deleteJob = async (monitor) => {
|
||||
this.scheduler.removeJob(monitor._id.toString());
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor) => {
|
||||
const result = this.scheduler.pauseJob(monitor._id.toString());
|
||||
const result = this.scheduler.pauseJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor._id}`,
|
||||
message: `Paused monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pauseJob",
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor) => {
|
||||
const result = this.scheduler.resumeJob(monitor._id.toString());
|
||||
const result = this.scheduler.resumeJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor._id}`,
|
||||
message: `Resumed monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "resumeJob",
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor) => {
|
||||
this.scheduler.updateJob(monitor._id.toString(), { repeat: monitor.interval, data: monitor.toObject() });
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
+2
-2
@@ -27,7 +27,7 @@ class SuperSimpleQueueHelper {
|
||||
getMonitorJob = () => {
|
||||
return async (monitor) => {
|
||||
try {
|
||||
const monitorId = monitor._id;
|
||||
const monitorId = monitor.id;
|
||||
const teamId = monitor.teamId;
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor id");
|
||||
@@ -62,7 +62,7 @@ class SuperSimpleQueueHelper {
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
details: `Error sending notifications for job ${monitor._id}: ${error.message}`,
|
||||
details: `Error sending notifications for job ${monitor.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
+6
-6
@@ -95,7 +95,7 @@ class NetworkService {
|
||||
}
|
||||
|
||||
const pingResponse = {
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: "ping",
|
||||
status: response.alive,
|
||||
code: 200,
|
||||
@@ -120,9 +120,9 @@ class NetworkService {
|
||||
}
|
||||
|
||||
async requestHttp(monitor) {
|
||||
const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const httpResponse = {
|
||||
monitorId: _id,
|
||||
monitorId: id,
|
||||
teamId: teamId,
|
||||
type,
|
||||
};
|
||||
@@ -298,7 +298,7 @@ class NetworkService {
|
||||
});
|
||||
|
||||
const dockerResponse = {
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: monitor.type,
|
||||
};
|
||||
|
||||
@@ -416,7 +416,7 @@ class NetworkService {
|
||||
code: 200,
|
||||
status: response.success,
|
||||
message: this.stringService.portSuccess,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: monitor.type,
|
||||
responseTime: responseTime,
|
||||
};
|
||||
@@ -444,7 +444,7 @@ class NetworkService {
|
||||
code: 200,
|
||||
status: true,
|
||||
message: "Success",
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: "game",
|
||||
};
|
||||
|
||||
+48
-28
@@ -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 { CheckModel } from "@/db/models/index.js";
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
class StatusService {
|
||||
@@ -7,14 +7,19 @@ class StatusService {
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* db: any
|
||||
* logger: any
|
||||
* buffer: import("./bufferService.js").BufferService
|
||||
* incidentService: import("../business/incidentService.js").IncidentService
|
||||
* monitorsRepository: any
|
||||
* }}
|
||||
*/ constructor({ db, logger, buffer, incidentService }) {
|
||||
*/
|
||||
constructor({ db, logger, buffer, incidentService, monitorsRepository }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
this.incidentService = incidentService;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -23,7 +28,7 @@ class StatusService {
|
||||
|
||||
async updateRunningStats({ monitor, networkResponse }) {
|
||||
try {
|
||||
const monitorId = monitor._id;
|
||||
const monitorId = monitor.id;
|
||||
const { responseTime, status } = networkResponse;
|
||||
// Get stats
|
||||
let stats = await MonitorStats.findOne({ monitorId });
|
||||
@@ -115,7 +120,7 @@ class StatusService {
|
||||
|
||||
if (!check._id) {
|
||||
try {
|
||||
const checkModel = new Check(check);
|
||||
const checkModel = new CheckModel(check);
|
||||
savedCheck = await checkModel.save();
|
||||
|
||||
this.buffer.removeCheckFromBuffer(check);
|
||||
@@ -124,7 +129,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleIncidentForCheck",
|
||||
message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
stack: checkError.stack,
|
||||
});
|
||||
savedCheck = null;
|
||||
@@ -139,7 +144,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleIncidentForCheck",
|
||||
message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
action,
|
||||
stack: incidentError.stack,
|
||||
});
|
||||
@@ -150,7 +155,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleIncidentForCheck",
|
||||
message: `Error in ${errorContext}: ${error.message}`,
|
||||
monitorId: monitor?._id,
|
||||
monitorId: monitor?.id,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
@@ -172,7 +177,8 @@ class StatusService {
|
||||
await this.insertCheck(check);
|
||||
try {
|
||||
const { monitorId, status, code } = networkResponse;
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId);
|
||||
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
@@ -198,9 +204,9 @@ class StatusService {
|
||||
|
||||
// Return early if not enough data points
|
||||
if (monitor.statusWindow.length < monitor.statusWindowSize) {
|
||||
await monitor.save();
|
||||
const updated = await this.monitorsRepository.update(monitor.id, monitor);
|
||||
return {
|
||||
monitor,
|
||||
monitor: updated,
|
||||
statusChanged: false,
|
||||
prevStatus,
|
||||
code,
|
||||
@@ -240,14 +246,14 @@ class StatusService {
|
||||
|
||||
if (monitor.status === false && !statusChanged) {
|
||||
try {
|
||||
const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor._id);
|
||||
const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor.id);
|
||||
|
||||
let calculatedFailureRate = failureRate;
|
||||
|
||||
if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) {
|
||||
try {
|
||||
const checksAfterResolution = await Check.find({
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
createdAt: { $gt: lastManuallyResolvedIncident.endTime },
|
||||
})
|
||||
.sort({ createdAt: 1 })
|
||||
@@ -267,7 +273,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "updateStatus",
|
||||
message: `Failed to query checks after manual resolution: ${checkQueryError.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
stack: checkQueryError.stack,
|
||||
});
|
||||
}
|
||||
@@ -281,17 +287,17 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "updateStatus",
|
||||
message: `Error handling threshold check without status change: ${error.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
monitor.status = newStatus;
|
||||
await monitor.save();
|
||||
const updated = await this.monitorsRepository.update(monitor.id, monitor);
|
||||
|
||||
return {
|
||||
monitor,
|
||||
monitor: updated,
|
||||
statusChanged,
|
||||
prevStatus,
|
||||
code,
|
||||
@@ -337,9 +343,11 @@ class StatusService {
|
||||
} = networkResponse;
|
||||
|
||||
const check = {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
},
|
||||
status,
|
||||
statusCode: code,
|
||||
responseTime,
|
||||
@@ -365,18 +373,30 @@ class StatusService {
|
||||
}
|
||||
const categories = payload?.lighthouseResult?.categories ?? {};
|
||||
const audits = payload?.lighthouseResult?.audits ?? {};
|
||||
const {
|
||||
"cumulative-layout-shift": cls = 0,
|
||||
"speed-index": si = 0,
|
||||
"first-contentful-paint": fcp = 0,
|
||||
"largest-contentful-paint": lcp = 0,
|
||||
"total-blocking-time": tbt = 0,
|
||||
} = audits;
|
||||
const mapAudit = (audit) => {
|
||||
if (!audit || typeof audit !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: audit.id,
|
||||
title: audit.title,
|
||||
score: typeof audit.score === "number" ? audit.score : (audit.score ?? null),
|
||||
displayValue: audit.displayValue,
|
||||
numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined,
|
||||
numericUnit: audit.numericUnit,
|
||||
};
|
||||
};
|
||||
check.accessibility = (categories?.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories?.seo?.score || 0) * 100;
|
||||
check.performance = (categories?.performance?.score || 0) * 100;
|
||||
check.audits = { cls, si, fcp, lcp, tbt };
|
||||
check.audits = {
|
||||
cls: mapAudit(audits?.["cumulative-layout-shift"]),
|
||||
si: mapAudit(audits?.["speed-index"]),
|
||||
fcp: mapAudit(audits?.["first-contentful-paint"]),
|
||||
lcp: mapAudit(audits?.["largest-contentful-paint"]),
|
||||
tbt: mapAudit(audits?.["total-blocking-time"]),
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "hardware") {
|
||||
@@ -1,317 +0,0 @@
|
||||
import { createMonitorsBodyValidation } from "../../../validation/joi.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
class MonitorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService, games }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
this.papaparse = papaparse;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
this.games = games;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MonitorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
verifyTeamAccess = async ({ teamId, monitorId }) => {
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
if (!monitor?.teamId?.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
};
|
||||
|
||||
getAllMonitors = async () => {
|
||||
const monitors = await this.db.monitorModule.getAllMonitors();
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const data = await this.db.monitorModule.getUptimeDetailsById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return monitorStats;
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange });
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getMonitorById = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
createMonitor = async ({ teamId, userId, body }) => {
|
||||
const monitor = await this.db.monitorModule.createMonitor({
|
||||
body,
|
||||
teamId,
|
||||
userId,
|
||||
});
|
||||
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
};
|
||||
|
||||
createBulkMonitors = async ({ fileData, userId, teamId }) => {
|
||||
const { parse } = this.papaparse;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value, header) => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }) => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw this.errorService.createServerError("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw this.errorService.createServerError("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
resolve(monitors);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deleteMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteAllMonitors = async ({ teamId }) => {
|
||||
const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId);
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
try {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.checkModule.deleteChecks(monitor._id);
|
||||
await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body });
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
};
|
||||
|
||||
pauseMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
addDemoMonitors = async ({ userId, teamId }) => {
|
||||
const demoMonitors = await this.db.monitorModule.addDemoMonitors(userId, teamId);
|
||||
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
|
||||
return demoMonitors;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }) => {
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
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 this.errorService.createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
teamId,
|
||||
});
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => {
|
||||
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
|
||||
type,
|
||||
explain,
|
||||
teamId,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
|
||||
const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
teamId,
|
||||
explain,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
exportMonitorsToCSV = async ({ teamId }) => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const csvData = monitors?.filteredMonitors?.map((monitor) => ({
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
type: monitor.type,
|
||||
url: monitor.url,
|
||||
interval: monitor.interval,
|
||||
port: monitor.port,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
isActive: monitor.isActive,
|
||||
}));
|
||||
|
||||
const csv = this.papaparse.unparse(csvData);
|
||||
return csv;
|
||||
};
|
||||
exportMonitorsToJSON = async ({ teamId }) => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const json = monitors?.filteredMonitors
|
||||
?.map((monitor) => {
|
||||
const initialType = monitor.type;
|
||||
let parsedType;
|
||||
|
||||
if (initialType === "hardware") {
|
||||
parsedType = "infrastructure";
|
||||
} else if (initialType === "http") {
|
||||
if (monitor.url.startsWith("https://")) {
|
||||
parsedType = "https";
|
||||
} else {
|
||||
parsedType = "http";
|
||||
}
|
||||
} else if (initialType === "pagespeed") {
|
||||
parsedType = initialType;
|
||||
} else {
|
||||
// Skip unsupported types
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
type: parsedType,
|
||||
interval: monitor.interval,
|
||||
n: monitor.statusWindowSize,
|
||||
secret: monitor.secret,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
getAllGames = () => {
|
||||
return this.games;
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async ({ teamId }) => {
|
||||
const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId });
|
||||
return groups;
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { MonitorType } from "@/types/index.js";
|
||||
|
||||
export interface CheckMetadata {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
type: MonitorType;
|
||||
}
|
||||
|
||||
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 {
|
||||
device: string;
|
||||
mountpoint: string;
|
||||
read_speed_bytes: number;
|
||||
write_speed_bytes: number;
|
||||
total_bytes: number;
|
||||
free_bytes: number;
|
||||
usage_percent: number;
|
||||
}
|
||||
|
||||
export interface CheckErrorInfo {
|
||||
metric: string[];
|
||||
err: string;
|
||||
}
|
||||
|
||||
export interface CheckNetworkInterfaceInfo {
|
||||
name: string;
|
||||
bytes_sent: number;
|
||||
bytes_recv: number;
|
||||
packets_sent: number;
|
||||
packets_recv: number;
|
||||
err_in: number;
|
||||
err_out: number;
|
||||
drop_in: number;
|
||||
drop_out: number;
|
||||
fifo_in: number;
|
||||
fifo_out: number;
|
||||
}
|
||||
|
||||
export interface CheckAudits {
|
||||
cls: ILighthouseAudit;
|
||||
si: ILighthouseAudit;
|
||||
fcp: ILighthouseAudit;
|
||||
lcp: ILighthouseAudit;
|
||||
tbt: ILighthouseAudit;
|
||||
}
|
||||
|
||||
export interface ILighthouseAudit {
|
||||
id?: string;
|
||||
title?: string;
|
||||
score?: number | null;
|
||||
displayValue?: string;
|
||||
numericValue?: number;
|
||||
numericUnit?: string;
|
||||
}
|
||||
|
||||
export interface Check {
|
||||
id: string;
|
||||
metadata: CheckMetadata;
|
||||
status: boolean;
|
||||
responseTime: number;
|
||||
timings: CheckTimings;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
ack: boolean;
|
||||
ackAt?: string | null;
|
||||
expiry: string;
|
||||
cpu: CheckCpuInfo;
|
||||
memory: CheckMemoryInfo;
|
||||
disk: CheckDiskInfo[];
|
||||
host: CheckHostInfo;
|
||||
errors: CheckErrorInfo[];
|
||||
capture: CheckCaptureInfo;
|
||||
net: CheckNetworkInterfaceInfo[];
|
||||
accessibility?: number;
|
||||
bestPractices?: number;
|
||||
seo?: number;
|
||||
performance?: number;
|
||||
audits?: CheckAudits;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user