Merge branch 'bluewave-labs:develop' into Network-tab-implementation-infra

This commit is contained in:
ThetaOI authored and GitHub committed 2025-08-05 19:49:38 +05:30
commit 02b7dc080b
118 files changed
+10748 -6684

No files matched your search

+5 -3
View File
@@ -8,11 +8,13 @@ import languageMiddleware from "./middleware/languageMiddleware.js";
import swaggerUi from "swagger-ui-express";
import { handleErrors } from "./middleware/handleErrors.js";
import { setupRoutes } from "./config/routes.js";
export const createApp = ({ services, controllers, appSettings, frontendPath, openApiSpec }) => {
const allowedOrigin = appSettings.clientHost;
import { generalApiLimiter } from "./middleware/rateLimiter.js";
export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => {
const allowedOrigin = envSettings.clientHost;
const app = express();
app.use(generalApiLimiter);
// Static files
app.use(express.static(frontendPath));
+2 -1
View File
@@ -1,4 +1,5 @@
import { verifyJWT } from "../middleware/verifyJWT.js";
import { authApiLimiter } from "../middleware/rateLimiter.js";
import AuthRoutes from "../routes/authRoute.js";
import InviteRoutes from "../routes/inviteRoute.js";
@@ -25,7 +26,7 @@ export const setupRoutes = (app, controllers) => {
const notificationRoutes = new NotificationRoutes(controllers.notificationController);
const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController);
app.use("/api/v1/auth", authRoutes.getRouter());
app.use("/api/v1/auth", authApiLimiter, authRoutes.getRouter());
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
+86 -9
View File
@@ -1,5 +1,4 @@
import ServiceRegistry from "../service/system/serviceRegistry.js";
import logger from "../utils/logger.js";
import TranslationService from "../service/system/translationService.js";
import StringService from "../service/system/stringService.js";
import MongoDB from "../db/mongo/MongoDB.js";
@@ -9,7 +8,6 @@ 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 RedisService from "../service/data/redisService.js";
import ErrorService from "../service/infrastructure/errorService.js";
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
@@ -19,7 +17,6 @@ import DiagnosticService from "../service/business/diagnosticService.js";
import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import MonitorService from "../service/business/monitorService.js";
import IORedis from "ioredis";
import papaparse from "papaparse";
import axios from "axios";
import ping from "ping";
@@ -35,19 +32,101 @@ import mjml2html from "mjml";
import jwt from "jsonwebtoken";
import crypto from "crypto";
export const initializeServices = async (appSettings, settingsService) => {
import { fileURLToPath } from "url";
import { ObjectId } from "mongodb";
// DB Modules
import { NormalizeData, NormalizeDataUptimeDetails } from "../utils/dataUtils.js";
import { GenerateAvatarImage } from "../utils/imageProcessing.js";
import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/models/Check.js";
import HardwareCheck from "../db/models/HardwareCheck.js";
import PageSpeedCheck from "../db/models/PageSpeedCheck.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 NetworkCheck from "../db/models/NetworkCheck.js";
import Notification from "../db/models/Notification.js";
import RecoveryToken from "../db/models/RecoveryToken.js";
import AppSettings from "../db/models/AppSettings.js";
import InviteModule from "../db/mongo/modules/inviteModule.js";
import CheckModule from "../db/mongo/modules/checkModule.js";
import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
import UserModule from "../db/mongo/modules/userModule.js";
import HardwareCheckModule from "../db/mongo/modules/hardwareCheckModule.js";
import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/mongo/modules/monitorModule.js";
import NetworkCheckModule from "../db/mongo/modules/networkCheckModule.js";
import NotificationModule from "../db/mongo/modules/notificationModule.js";
import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js";
import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
import SettingsModule from "../db/mongo/modules/settingsModule.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
ServiceRegistry.instance = serviceRegistry;
const translationService = new TranslationService(logger);
await translationService.initialize();
const stringService = new StringService(translationService);
// Create DB
const db = new MongoDB({ appSettings });
const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User });
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger });
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
const monitorModule = new MonitorModule({
Monitor,
MonitorStats,
Check,
PageSpeedCheck,
HardwareCheck,
stringService,
fs,
path,
fileURLToPath,
ObjectId,
NormalizeData,
NormalizeDataUptimeDetails,
});
const networkCheckModule = new NetworkCheckModule({ NetworkCheck });
const notificationModule = new NotificationModule({ Notification, Monitor });
const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck });
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
const settingsModule = new SettingsModule({ AppSettings });
const db = new MongoDB({
logger,
envSettings,
checkModule,
inviteModule,
statusPageModule,
userModule,
hardwareCheckModule,
maintenanceWindowModule,
monitorModule,
networkCheckModule,
notificationModule,
pageSpeedCheckModule,
recoveryModule,
settingsModule,
});
await db.connect();
const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService);
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
const bufferService = new BufferService({ db, logger });
const bufferService = new BufferService({ db, logger, envSettings });
const statusService = new StatusService({ db, logger, buffer: bufferService });
const notificationUtils = new NotificationUtils({
@@ -64,7 +143,6 @@ export const initializeServices = async (appSettings, settingsService) => {
notificationUtils,
});
const redisService = new RedisService({ Redis: IORedis, logger });
const errorService = new ErrorService();
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
@@ -76,7 +154,7 @@ export const initializeServices = async (appSettings, settingsService) => {
});
const superSimpleQueue = await SuperSimpleQueue.create({
appSettings,
envSettings,
db,
logger,
helper: superSimpleQueueHelper,
@@ -135,7 +213,6 @@ export const initializeServices = async (appSettings, settingsService) => {
bufferService,
statusService,
notificationService,
redisService,
jobQueue: superSimpleQueue,
userService,
checkService,
+2 -2
View File
@@ -32,7 +32,7 @@ class MonitorController extends BaseController {
}
async verifyTeamAccess(teamId, monitorId) {
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
@@ -150,7 +150,7 @@ class MonitorController extends BaseController {
await getCertificateParamValidation.validateAsync(req.params);
const { monitorId } = req.params;
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
return res.success({
@@ -53,7 +53,7 @@ class NotificationController extends BaseController {
body.userId = userId;
body.teamId = teamId;
const notification = await this.db.createNotification(body);
const notification = await this.db.notificationModule.createNotification(body);
return res.success({
msg: "Notification created successfully",
data: notification,
@@ -70,7 +70,7 @@ class NotificationController extends BaseController {
throw this.errorService.createBadRequestError("Team ID is required");
}
const notifications = await this.db.getNotificationsByTeamId(teamId);
const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId);
return res.success({
msg: "Notifications fetched successfully",
@@ -88,12 +88,12 @@ class NotificationController extends BaseController {
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
const notification = await this.db.notificationModule.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
await this.db.deleteNotificationById(req.params.id);
await this.db.notificationModule.deleteNotificationById(req.params.id);
return res.success({
msg: "Notification deleted successfully",
});
@@ -104,7 +104,7 @@ class NotificationController extends BaseController {
getNotificationById = this.asyncHandler(
async (req, res) => {
const notification = await this.db.getNotificationById(req.params.id);
const notification = await this.db.notificationModule.getNotificationById(req.params.id);
const teamId = req?.user?.teamId;
if (!teamId) {
@@ -134,13 +134,13 @@ class NotificationController extends BaseController {
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
const notification = await this.db.notificationModule.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
const editedNotification = await this.db.editNotification(req.params.id, req.body);
const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body);
return res.success({
msg: "Notification updated successfully",
data: editedNotification,
@@ -158,7 +158,7 @@ class NotificationController extends BaseController {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
+1 -1
View File
@@ -55,7 +55,7 @@ class SettingsController extends BaseController {
async (req, res) => {
await updateAppSettingsBodyValidation.validateAsync(req.body);
const updatedSettings = await this.db.updateAppSettings(req.body);
const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body);
const returnSettings = this.buildAppSettings(updatedSettings);
return res.success({
msg: this.stringService.updateAppSettings,
@@ -19,7 +19,7 @@ class StatusPageController extends BaseController {
await imageValidation.validateAsync(req.file);
const { _id, teamId } = req.user;
const statusPage = await this.db.createStatusPage({
const statusPage = await this.db.statusPageModule.createStatusPage({
statusPageData: req.body,
image: req.file,
userId: _id,
@@ -39,7 +39,7 @@ class StatusPageController extends BaseController {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
const statusPage = await this.db.updateStatusPage(req.body, req.file);
const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file);
if (statusPage === null) {
throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound);
}
@@ -54,7 +54,7 @@ class StatusPageController extends BaseController {
getStatusPage = this.asyncHandler(
async (req, res) => {
const statusPage = await this.db.getStatusPage();
const statusPage = await this.db.statusPageModule.getStatusPage();
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
@@ -69,7 +69,7 @@ class StatusPageController extends BaseController {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type);
const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url);
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
@@ -82,7 +82,7 @@ class StatusPageController extends BaseController {
getStatusPagesByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req.user.teamId;
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId);
return res.success({
msg: this.stringService.statusPageByTeamId,
+6
View File
@@ -65,6 +65,12 @@ const AppSettingsSchema = mongoose.Schema(
type: Number,
default: 1,
},
globalThresholds: {
cpu: { type: Number },
memory: { type: Number },
disk: { type: Number },
temperature: { type: Number },
},
},
{
timestamps: true,
+4
View File
@@ -74,6 +74,10 @@ const StatusPageSchema = mongoose.Schema(
type: Boolean,
default: false,
},
customCSS: {
type: String,
default: "",
},
},
{ timestamps: true }
);
-8
View File
@@ -103,12 +103,4 @@ UserSchema.methods.comparePassword = async function (submittedPassword) {
const User = mongoose.model("User", UserSchema);
User.init().then(() => {
logger.info({
message: "User model initialized",
service: "UserModel",
method: "init",
});
});
export default User;
+37 -87
View File
@@ -1,89 +1,39 @@
import mongoose from "mongoose";
import AppSettings from "../models/AppSettings.js";
import logger from "../../utils/logger.js";
//****************************************
// User Operations
//****************************************
import * as userModule from "./modules/userModule.js";
//****************************************
// Invite Token Operations
//****************************************
import * as inviteModule from "./modules/inviteModule.js";
//****************************************
// Recovery Operations
//****************************************
import * as recoveryModule from "./modules/recoveryModule.js";
//****************************************
// Monitors
//****************************************
import * as monitorModule from "./modules/monitorModule.js";
//****************************************
// Page Speed Checks
//****************************************
import * as pageSpeedCheckModule from "./modules/pageSpeedCheckModule.js";
//****************************************
// Hardware Checks
//****************************************
import * as hardwareCheckModule from "./modules/hardwareCheckModule.js";
//****************************************
// Checks
//****************************************
import * as checkModule from "./modules/checkModule.js";
//****************************************
// Maintenance Window
//****************************************
import * as maintenanceWindowModule from "./modules/maintenanceWindowModule.js";
//****************************************
// Notifications
//****************************************
import * as notificationModule from "./modules/notificationModule.js";
//****************************************
// AppSettings
//****************************************
import * as settingsModule from "./modules/settingsModule.js";
//****************************************
// Status Page
//****************************************
import * as statusPageModule from "./modules/statusPageModule.js";
//****************************************
// Diagnostic
//****************************************
import * as diagnosticModule from "./modules/diagnosticModule.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";
constructor({ appSettings }) {
this.appSettings = appSettings;
Object.assign(this, userModule);
Object.assign(this, inviteModule);
Object.assign(this, recoveryModule);
Object.assign(this, monitorModule);
Object.assign(this, pageSpeedCheckModule);
Object.assign(this, hardwareCheckModule);
Object.assign(this, checkModule);
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);
Object.assign(this, statusPageModule);
Object.assign(this, diagnosticModule);
constructor({
logger,
envSettings,
checkModule,
inviteModule,
statusPageModule,
userModule,
hardwareCheckModule,
maintenanceWindowModule,
monitorModule,
networkCheckModule,
notificationModule,
pageSpeedCheckModule,
recoveryModule,
settingsModule,
}) {
this.logger = logger;
this.envSettings = envSettings;
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;
}
get serviceName() {
@@ -92,9 +42,9 @@ class MongoDB {
connect = async () => {
try {
const connectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
await mongoose.connect(connectionString);
// If there are no AppSettings, create one
// If there are no AppSettings, create one // TODO why is this here?
await AppSettings.findOneAndUpdate(
{}, // empty filter to match any document
{}, // empty update
@@ -110,13 +60,13 @@ class MongoDB {
await model.syncIndexes();
}
logger.info({
this.logger.info({
message: "Connected to MongoDB",
service: this.SERVICE_NAME,
method: "connect",
});
} catch (error) {
logger.error({
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "connect",
@@ -128,12 +78,12 @@ class MongoDB {
disconnect = async () => {
try {
logger.info({ message: "Disconnecting from MongoDB" });
this.logger.info({ message: "Disconnecting from MongoDB" });
await mongoose.disconnect();
logger.info({ message: "Disconnected from MongoDB" });
this.logger.info({ message: "Disconnected from MongoDB" });
return;
} catch (error) {
logger.error({
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "disconnect",
+277 -356
View File
@@ -1,9 +1,3 @@
import Check from "../../models/Check.js";
import Monitor from "../../models/Monitor.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import User from "../../models/User.js";
import logger from "../../../utils/logger.js";
import { ObjectId } from "mongodb";
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
@@ -17,369 +11,296 @@ const dateRangeLookup = {
all: undefined,
};
/**
* Create a check for a monitor
* @async
* @param {Object} checkData
* @param {string} checkData.monitorId
* @param {boolean} checkData.status
* @param {number} checkData.responseTime
* @param {number} checkData.statusCode
* @param {string} checkData.message
* @returns {Promise<Check>}
* @throws {Error}
*/
const createCheck = async (checkData) => {
try {
const check = await new Check({ ...checkData }).save();
return check;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
throw error;
class CheckModule {
constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) {
this.logger = logger;
this.Check = Check;
this.HardwareCheck = HardwareCheck;
this.PageSpeedCheck = PageSpeedCheck;
this.Monitor = Monitor;
this.User = User;
}
};
const createChecks = async (checks) => {
try {
await Check.insertMany(checks, { ordered: false });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
throw error;
}
};
/**
* Get all checks for a monitor
* @async
* @param {string} monitorId
* @returns {Promise<Array<Check>>}
* @throws {Error}
*/
const getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
try {
status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
// Match
const matchStage = {
monitorId: new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
matchStage.statusCode = 5000;
break;
default:
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecks",
});
break;
}
createChecks = async (checks) => {
try {
await this.Check.insertMany(checks, { ordered: false });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
throw error;
}
};
//Sort
sortOrder = sortOrder === "asc" ? 1 : -1;
getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
try {
status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
const checkModels = {
http: Check,
ping: Check,
docker: Check,
port: Check,
pagespeed: PageSpeedCheck,
hardware: HardwareCheck,
};
const Model = checkModels[type];
const checks = await Model.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: {
$ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0],
// Match
const matchStage = {
monitorId: new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
checks: {
$ifNull: ["$checks", []],
}),
};
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
matchStage.statusCode = 5000;
break;
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecks",
});
break;
}
}
//Sort
sortOrder = sortOrder === "asc" ? 1 : -1;
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const checkModels = {
http: this.Check,
ping: this.Check,
docker: this.Check,
port: this.Check,
pagespeed: this.PageSpeedCheck,
hardware: this.HardwareCheck,
};
const Model = checkModels[type];
const checks = await Model.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
},
]);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecks";
throw error;
}
};
const getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => {
try {
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
const matchStage = {
teamId: new ObjectId(teamId),
status: false,
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
{
$project: {
checksCount: {
$ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0],
},
checks: {
$ifNull: ["$checks", []],
},
},
},
}),
};
// Add filter to match stage
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
matchStage.statusCode = 5000;
break;
default:
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecksByTeam",
});
break;
]);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecks";
throw error;
}
};
getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => {
try {
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
const matchStage = {
teamId: new ObjectId(teamId),
status: false,
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
// Add filter to match stage
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
matchStage.statusCode = 5000;
break;
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecksByTeam",
});
break;
}
}
sortOrder = sortOrder === "asc" ? 1 : -1;
// pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const aggregatePipeline = [
{ $match: matchStage },
{
$unionWith: {
coll: "hardwarechecks",
pipeline: [{ $match: matchStage }],
},
},
{
$unionWith: {
coll: "pagespeedchecks",
pipeline: [{ $match: matchStage }],
},
},
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
checks: "$checks",
},
},
];
const checks = await this.Check.aggregate(aggregatePipeline);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksByTeam";
throw error;
}
};
ackCheck = async (checkId, teamId, ack) => {
try {
const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true });
if (!updatedCheck) {
throw new Error("Check not found");
}
return updatedCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "ackCheck";
throw error;
}
};
ackAllChecks = async (monitorId, teamId, ack, path) => {
try {
const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } });
return updatedChecks.modifiedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "ackAllChecks";
throw error;
}
};
getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
};
const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksSummaryByTeamId";
throw error;
}
};
deleteChecks = async (monitorId) => {
try {
const result = await this.Check.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecks";
throw error;
}
};
deleteChecksByTeamId = async (teamId) => {
try {
// Find all monitor IDs for this team (only get _id field for efficiency)
const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 });
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 } });
return deleteResult.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecksByTeamId";
throw error;
}
};
updateChecksTTL = async (teamId, ttl) => {
try {
await this.Check.collection.dropIndex("expiry_1");
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "updateChecksTTL",
stack: error.stack,
});
}
sortOrder = sortOrder === "asc" ? 1 : -1;
// pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
try {
await this.Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
const aggregatePipeline = [
{ $match: matchStage },
{
$unionWith: {
coll: "hardwarechecks",
pipeline: [{ $match: matchStage }],
},
},
{
$unionWith: {
coll: "pagespeedchecks",
pipeline: [{ $match: matchStage }],
},
},
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
checks: "$checks",
},
},
];
const checks = await Check.aggregate(aggregatePipeline);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksByTeam";
throw error;
}
};
/**
* Update the acknowledgment status of a check
* @async
* @param {string} checkId - The ID of the check to update
* @param {string} teamId - The ID of the team
* @param {boolean} ack - The acknowledgment status to set
* @returns {Promise<Check>}
* @throws {Error}
*/
const ackCheck = async (checkId, teamId, ack) => {
try {
const updatedCheck = await Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true });
if (!updatedCheck) {
throw new Error("Check not found");
// Update user
try {
await this.User.updateMany({ teamId: teamId }, { checkTTL: ttl });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
};
}
return updatedCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "ackCheck";
throw error;
}
};
/**
* Update the acknowledgment status of all checks for a monitor or team
* @async
* @param {string} id - The monitor ID or team ID
* @param {boolean} ack - The acknowledgment status to set
* @param {string} path - The path type ('monitor' or 'team')
* @returns {Promise<number>}
* @throws {Error}
*/
const ackAllChecks = async (monitorId, teamId, ack, path) => {
try {
const updatedChecks = await Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } });
return updatedChecks.modifiedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "ackAllChecks";
throw error;
}
};
/**
* Get checks and summary by team ID
* @async
* @param {string} teamId
* @returns {Promise<Object>}
* @throws {Error}
*/
const getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
};
const checks = await Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksSummaryByTeamId";
throw error;
}
};
/**
* Delete all checks for a monitor
* @async
* @param {string} monitorId
* @returns {number}
* @throws {Error}
*/
const deleteChecks = async (monitorId) => {
try {
const result = await Check.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecks";
throw error;
}
};
/**
* Delete all checks for a team
* @async
* @param {string} monitorId
* @returns {number}
* @throws {Error}
*/
const deleteChecksByTeamId = async (teamId) => {
try {
// Find all monitor IDs for this team (only get _id field for efficiency)
const teamMonitors = await Monitor.find({ teamId }, { _id: 1 });
const monitorIds = teamMonitors.map((monitor) => monitor._id);
// Delete all checks for these monitors in one operation
const deleteResult = await Check.deleteMany({ monitorId: { $in: monitorIds } });
return deleteResult.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecksByTeamId";
throw error;
}
};
const updateChecksTTL = async (teamId, ttl) => {
try {
await Check.collection.dropIndex("expiry_1");
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "updateChecksTTL",
stack: error.stack,
});
}
try {
await Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
// Update user
try {
await User.updateMany({ teamId: teamId }, { checkTTL: ttl });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
};
export {
createCheck,
createChecks,
getChecksByMonitor,
getChecksByTeam,
ackCheck,
ackAllChecks,
getChecksSummaryByTeamId,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
};
export default CheckModule;
@@ -1,48 +0,0 @@
import Monitor from "../../models/Monitor.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "diagnosticModule";
import { buildMonitorSummaryByTeamIdPipeline, buildMonitorsByTeamIdPipeline, buildFilteredMonitorsByTeamIdPipeline } from "./monitorModuleQueries.js";
const getMonitorsByTeamIdExecutionStats = async (req) => {
try {
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 match stage
const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const summary = await Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })).explain("executionStats");
const monitors = await Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })).explain("executionStats");
const filteredMonitors = await Monitor.aggregate(
buildFilteredMonitorsByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
).explain("executionStats");
return { summary, monitors, filteredMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorSummaryByTeamIdExecutionStats";
throw error;
}
};
export { getMonitorsByTeamIdExecutionStats };
@@ -1,70 +1,22 @@
import HardwareCheck from "../../models/HardwareCheck.js";
import Monitor from "../../models/Monitor.js";
import logger from "../../../utils/logger.js";
const SERVICE_NAME = "hardwareCheckModule";
const createHardwareCheck = async (hardwareCheckData) => {
try {
const { monitorId, status } = hardwareCheckData;
const n = (await HardwareCheck.countDocuments({ monitorId })) + 1;
const monitor = await Monitor.findById(monitorId);
if (!monitor) {
logger.error({
message: "Monitor not found",
service: SERVICE_NAME,
method: "createHardwareCheck",
details: `monitor ID: ${monitorId}`,
});
return null;
class HardwareCheckModule {
constructor({ HardwareCheck, Monitor, logger }) {
this.HardwareCheck = HardwareCheck;
this.Monitor = Monitor;
this.logger = logger;
}
createHardwareChecks = async (hardwareChecks) => {
try {
await this.HardwareCheck.insertMany(hardwareChecks, { ordered: false });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createHardwareChecks";
throw error;
}
};
}
let newUptimePercentage;
if (monitor.uptimePercentage === undefined) {
newUptimePercentage = status === true ? 1 : 0;
} else {
newUptimePercentage = (monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n;
}
await Monitor.findOneAndUpdate({ _id: monitorId }, { uptimePercentage: newUptimePercentage });
const hardwareCheck = await new HardwareCheck({
...hardwareCheckData,
}).save();
return hardwareCheck;
} catch (error) {
logger.error({
message: "Error creating hardware check",
service: SERVICE_NAME,
method: "createHardwareCheck",
stack: error.stack,
});
error.service = SERVICE_NAME;
error.method = "createHardwareCheck";
throw error;
}
};
const createHardwareChecks = async (hardwareChecks) => {
try {
await HardwareCheck.insertMany(hardwareChecks, { ordered: false });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createHardwareChecks";
throw error;
}
};
const deleteHardwareChecksByMonitorId = async (monitorId) => {
try {
const result = await HardwareCheck.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteHardwareChecksByMonitorId";
throw error;
}
};
export { createHardwareCheck, createHardwareChecks, deleteHardwareChecksByMonitorId };
export default HardwareCheckModule;
+50 -83
View File
@@ -1,89 +1,56 @@
import InviteToken from "../../models/InviteToken.js";
import crypto from "crypto";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "inviteModule";
/**
* Request an invite token for a user.
*
* This function deletes any existing invite tokens for the user's email,
* generates a new token, saves it, and then returns the new token.
*
* @param {Object} userData - The user data.
* @param {string} userData.email - The user's email.
* @param {mongoose.Schema.Types.ObjectId} userData.teamId - The ID of the team.
* @param {Array} userData.role - The user's role(s).
* @param {Date} [userData.expiry=Date.now] - The expiry date of the token. Defaults to the current date and time.
* @returns {Promise<InviteToken>} The invite token.
* @throws {Error} If there is an error.
*/
const requestInviteToken = async (userData) => {
try {
await InviteToken.deleteMany({ email: userData.email });
userData.token = crypto.randomBytes(32).toString("hex");
let inviteToken = new InviteToken(userData);
await inviteToken.save();
return inviteToken;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "requestInviteToken";
throw error;
}
};
/**
* Retrieves an invite token
*
* This function searches for an invite token in the database and deletes it.
* If the invite token is not found, it throws an error.
*
* @param {string} token - The invite token to search for.
* @returns {Promise<InviteToken>} The invite token data.
* @throws {Error} If the invite token is not found or there is another error.
*/
const getInviteToken = async (token) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const invite = await InviteToken.findOne({
token,
});
if (invite === null) {
throw new Error(stringService.authInviteNotFound);
class InviteModule {
constructor({ InviteToken, crypto, stringService }) {
this.InviteToken = InviteToken;
this.crypto = crypto;
this.stringService = stringService;
}
requestInviteToken = async (userData) => {
try {
await this.InviteToken.deleteMany({ email: userData.email });
userData.token = this.crypto.randomBytes(32).toString("hex");
let inviteToken = new this.InviteToken(userData);
await inviteToken.save();
return inviteToken;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "requestInviteToken";
throw error;
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteToken";
throw error;
}
};
};
/**
* Retrieves and deletes an invite token
*
* This function searches for an invite token in the database and deletes it.
* If the invite token is not found, it throws an error.
*
* @param {string} token - The invite token to search for.
* @returns {Promise<InviteToken>} The invite token data.
* @throws {Error} If the invite token is not found or there is another error.
*/
const getInviteTokenAndDelete = async (token) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const invite = await InviteToken.findOneAndDelete({
token,
});
if (invite === null) {
throw new Error(stringService.authInviteNotFound);
getInviteToken = async (token) => {
try {
const invite = await this.InviteToken.findOne({
token,
});
if (invite === null) {
throw new Error(this.stringService.authInviteNotFound);
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteToken";
throw error;
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteTokenAndDelete";
throw error;
}
};
};
getInviteTokenAndDelete = async (token) => {
try {
const invite = await this.InviteToken.findOneAndDelete({
token,
});
if (invite === null) {
throw new Error(this.stringService.authInviteNotFound);
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteTokenAndDelete";
throw error;
}
};
}
export { requestInviteToken, getInviteToken, getInviteTokenAndDelete };
export default InviteModule;
@@ -1,192 +1,106 @@
import MaintenanceWindow from "../../models/MaintenanceWindow.js";
const SERVICE_NAME = "maintenanceWindowModule";
/**
* Asynchronously creates a new MaintenanceWindow document and saves it to the database.
* If the maintenance window is a one-time event, the expiry field is set to the same value as the end field.
* @async
* @function createMaintenanceWindow
* @param {Object} maintenanceWindowData - The data for the new MaintenanceWindow document.
* @param {mongoose.Schema.Types.ObjectId} maintenanceWindowData.monitorId - The ID of the monitor.
* @param {Boolean} maintenanceWindowData.active - Indicates whether the maintenance window is active.
* @param {Boolean} maintenanceWindowData.oneTime - Indicates whether the maintenance window is a one-time event.
* @param {Date} maintenanceWindowData.start - The start date and time of the maintenance window.
* @param {Date} maintenanceWindowData.end - The end date and time of the maintenance window.
* @returns {Promise<MaintenanceWindow>} The saved MaintenanceWindow document.
* @throws {Error} If there is an error saving the document.
*/
const createMaintenanceWindow = async (maintenanceWindowData) => {
try {
const maintenanceWindow = new MaintenanceWindow({
...maintenanceWindowData,
});
class MaintenanceWindowModule {
constructor({ MaintenanceWindow }) {
this.MaintenanceWindow = MaintenanceWindow;
}
createMaintenanceWindow = async (maintenanceWindowData) => {
try {
const maintenanceWindow = new this.MaintenanceWindow({
...maintenanceWindowData,
});
// If the maintenance window is a one time window, set the expiry to the end date
if (maintenanceWindowData.oneTime) {
maintenanceWindow.expiry = maintenanceWindowData.end;
// If the maintenance window is a one time window, set the expiry to the end date
if (maintenanceWindowData.oneTime) {
maintenanceWindow.expiry = maintenanceWindowData.end;
}
const result = await maintenanceWindow.save();
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMaintenanceWindow";
throw error;
}
const result = await maintenanceWindow.save();
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMaintenanceWindow";
throw error;
}
};
const getMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await MaintenanceWindow.findOne({
_id: id,
teamId: teamId,
});
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowById";
throw error;
}
};
/**
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific team ID.
* @async
* @function getMaintenanceWindowByUserId
* @param {String} teamId - The ID of the team.
* @param {Object} query - The request body.
* @returns {Promise<Array<MaintenanceWindow>>} An array of MaintenanceWindow documents.
* @throws {Error} If there is an error retrieving the documents.
*/
const getMaintenanceWindowsByTeamId = async (teamId, query) => {
try {
let { active, page, rowsPerPage, field, order } = query || {};
const maintenanceQuery = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
const maintenanceWindowCount = await MaintenanceWindow.countDocuments(maintenanceQuery);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
};
getMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await this.MaintenanceWindow.findOne({
_id: id,
teamId: teamId,
});
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowById";
throw error;
}
};
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
getMaintenanceWindowsByTeamId = async (teamId, query) => {
try {
let { active, page, rowsPerPage, field, order } = query || {};
const maintenanceQuery = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
const maintenanceWindowCount = await this.MaintenanceWindow.countDocuments(maintenanceQuery);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await this.MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort);
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
throw error;
}
};
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
try {
const maintenanceWindows = await this.MaintenanceWindow.find({
monitorId: monitorId,
teamId: teamId,
});
return maintenanceWindows;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowsByMonitorId";
throw error;
}
};
const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort);
deleteMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await this.MaintenanceWindow.findOneAndDelete({ _id: id, teamId });
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenanceWindowById";
throw error;
}
};
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
throw error;
}
};
editMaintenanceWindowById = async ({ id, body }) => {
try {
const editedMaintenanceWindow = await this.MaintenanceWindow.findByIdAndUpdate(id, body, { new: true });
return editedMaintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMaintenanceWindowById";
throw error;
}
};
}
/**
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific monitor ID.
* @async
* @function getMaintenanceWindowsByMonitorId
* @param {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor.
* @returns {Promise<Array<MaintenanceWindow>>} An array of MaintenanceWindow documents.
* @throws {Error} If there is an error retrieving the documents.
*/
const getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
try {
const maintenanceWindows = await MaintenanceWindow.find({
monitorId: monitorId,
teamId: teamId,
});
return maintenanceWindows;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowsByMonitorId";
throw error;
}
};
/**
* Asynchronously deletes a MaintenanceWindow document by its ID.
* @async
* @function deleteMaintenanceWindowById
* @param {mongoose.Schema.Types.ObjectId} maintenanceWindowId - The ID of the MaintenanceWindow document to delete.
* @returns {Promise<MaintenanceWindow>} The deleted MaintenanceWindow document.
* @throws {Error} If there is an error deleting the document.
*/
const deleteMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await MaintenanceWindow.findOneAndDelete({ _id: id, teamId });
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenanceWindowById";
throw error;
}
};
/**
* Asynchronously deletes all MaintenanceWindow documents associated with a specific monitor ID.
* @async
* @function deleteMaintenanceWindowByMonitorId
* @param {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor.
* @returns {Promise<Object>} The result of the delete operation. This object contains information about the operation, such as the number of documents deleted.
* @throws {Error} If there is an error deleting the documents.
* @example
*/
const deleteMaintenanceWindowByMonitorId = async (monitorId) => {
try {
const result = await MaintenanceWindow.deleteMany({ monitorId: monitorId });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenanceWindowByMonitorId";
throw error;
}
};
/**
* Asynchronously deletes all MaintenanceWindow documents associated with a specific user ID.
* @async
* @function deleteMaintenanceWindowByUserId
* @param {String} userId - The ID of the user.
* @returns {Promise<Object>} The result of the delete operation. This object contains information about the operation, such as the number of documents deleted.
* @throws {Error} If there is an error deleting the documents.
* @example
*/
const deleteMaintenanceWindowByUserId = async (userId) => {
try {
const result = await MaintenanceWindow.deleteMany({ userId: userId });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenanceWindowByUserId";
throw error;
}
};
const editMaintenanceWindowById = async ({ id, body, teamId }) => {
try {
const editedMaintenanceWindow = await MaintenanceWindow.findByIdAndUpdate(id, body, { new: true });
return editedMaintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMaintenanceWindowById";
throw error;
}
};
export {
createMaintenanceWindow,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
};
export default MaintenanceWindowModule;
File diff suppressed because it is too large. Load diff
@@ -1,44 +1,30 @@
import NetworkCheck from "../../models/NetworkCheck.js";
const SERVICE_NAME = "networkCheckModule";
/**
* Creates and saves a new network check document to the database.
* @async
* @param {object} networkCheckData - The data for the new network check. This should conform to the NetworkCheckSchema.
* @param {string} networkCheckData.monitorId - The ID of the monitor associated with this check.
* @returns {Promise<object>} A promise that resolves to the newly created network check document.
* @throws {Error} Throws an error if the database operation fails.
*/
const createNetworkCheck = async (networkCheckData) => {
try {
const networkCheck = await new NetworkCheck(networkCheckData);
await networkCheck.save();
return networkCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNetworkCheck";
throw error;
class NetworkCheckModule {
constructor({ NetworkCheck }) {
this.NetworkCheck = NetworkCheck;
}
};
createNetworkCheck = async (networkCheckData) => {
try {
const networkCheck = await new this.NetworkCheck(networkCheckData);
await networkCheck.save();
return networkCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNetworkCheck";
throw error;
}
};
getNetworkChecksByMonitorId = async (monitorId, limit = 100) => {
try {
const networkChecks = await this.NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit);
return networkChecks;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNetworkChecksByMonitorId";
throw error;
}
};
}
/**
* Retrieves a list of network checks for a specific monitor, sorted by most recent.
* @async
* @param {string} monitorId - The ID of the monitor to retrieve checks for.
* @param {number} [limit=100] - The maximum number of checks to return. Defaults to 100.
* @returns {Promise<Array<object>>} A promise that resolves to an array of network check documents.
* @throws {Error} Throws an error if the database operation fails.
*/
const getNetworkChecksByMonitorId = async (monitorId, limit = 100) => {
try {
const networkChecks = await NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit);
return networkChecks;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNetworkChecksByMonitorId";
throw error;
}
};
export { createNetworkCheck, getNetworkChecksByMonitorId };
export default NetworkCheckModule;
+94 -119
View File
@@ -1,125 +1,100 @@
import Notification from "../../models/Notification.js";
import Monitor from "../../models/Monitor.js";
// import Notification from "../../models/Notification.js";
// import Monitor from "../../models/Monitor.js";
const SERVICE_NAME = "notificationModule";
/**
* Creates a new notification.
* @param {Object} notificationData - The data for the new notification.
* @param {mongoose.Types.ObjectId} notificationData.monitorId - The ID of the monitor.
* @param {string} notificationData.type - The type of the notification (e.g., "email", "sms").
* @param {string} [notificationData.address] - The address for the notification (if applicable).
* @param {string} [notificationData.phone] - The phone number for the notification (if applicable).
* @returns {Promise<Object>} The created notification.
* @throws Will throw an error if the notification cannot be created.
*/
const createNotification = async (notificationData) => {
try {
const notification = await new Notification({ ...notificationData }).save();
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNotification";
throw error;
}
};
const getNotificationsByTeamId = async (teamId) => {
try {
const notifications = await Notification.find({ teamId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByTeamId";
throw error;
class NotificationModule {
constructor({ Notification, Monitor }) {
this.Notification = Notification;
this.Monitor = Monitor;
}
};
const getNotificationsByIds = async (notificationIds) => {
try {
const notifications = await Notification.find({ _id: { $in: notificationIds } });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByIds";
throw error;
}
};
/**
* Retrieves notifications by monitor ID.
* @param {mongoose.Types.ObjectId} monitorId - The ID of the monitor.
* @returns {Promise<Array<Object>>} An array of notifications.
* @throws Will throw an error if the notifications cannot be retrieved.
*/
const getNotificationsByMonitorId = async (monitorId) => {
try {
const notifications = await Notification.find({ monitorId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByMonitorId";
throw error;
}
};
const deleteNotificationsByMonitorId = async (monitorId) => {
try {
const result = await Notification.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationsByMonitorId";
throw error;
}
};
const deleteNotificationById = async (id) => {
try {
const notification = await Notification.findById(id);
if (!notification) {
throw new Error("Notification not found");
createNotification = async (notificationData) => {
try {
const notification = await new this.Notification({ ...notificationData }).save();
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNotification";
throw error;
}
};
getNotificationsByTeamId = async (teamId) => {
try {
const notifications = await this.Notification.find({ teamId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByTeamId";
throw error;
}
};
getNotificationsByIds = async (notificationIds) => {
try {
const notifications = await this.Notification.find({ _id: { $in: notificationIds } });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByIds";
throw error;
}
};
getNotificationsByMonitorId = async (monitorId) => {
try {
const notifications = await this.Notification.find({ monitorId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByMonitorId";
throw error;
}
};
deleteNotificationsByMonitorId = async (monitorId) => {
try {
const result = await this.Notification.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationsByMonitorId";
throw error;
}
};
deleteNotificationById = async (id) => {
try {
const notification = await this.Notification.findById(id);
if (!notification) {
throw new Error("Notification not found");
}
const result = await Notification.findByIdAndDelete(id);
await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationById";
throw error;
}
};
const result = await this.Notification.findByIdAndDelete(id);
await this.Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationById";
throw error;
}
};
getNotificationById = async (id) => {
try {
const notification = await this.Notification.findById(id);
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationById";
throw error;
}
};
editNotification = async (id, notificationData) => {
try {
const notification = await this.Notification.findByIdAndUpdate(id, notificationData, {
new: true,
});
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editNotification";
throw error;
}
};
}
const getNotificationById = async (id) => {
try {
const notification = await Notification.findById(id);
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationById";
throw error;
}
};
const editNotification = async (id, notificationData) => {
try {
const notification = await Notification.findByIdAndUpdate(id, notificationData, {
new: true,
});
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editNotification";
throw error;
}
};
export {
createNotification,
getNotificationsByTeamId,
getNotificationsByIds,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
deleteNotificationById,
getNotificationById,
editNotification,
};
export default NotificationModule;
@@ -1,57 +1,31 @@
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
// import PageSpeedCheck from "../../models/PageSpeedCheck.js";
const SERVICE_NAME = "pageSpeedCheckModule";
/**
* Create a PageSpeed check for a monitor
* @async
* @param {Object} pageSpeedCheckData
* @param {string} pageSpeedCheckData.monitorId
* @param {number} pageSpeedCheckData.accessibility
* @param {number} pageSpeedCheckData.bestPractices
* @param {number} pageSpeedCheckData.seo
* @param {number} pageSpeedCheckData.performance
* @returns {Promise<PageSpeedCheck>}
* @throws {Error}
*/
const createPageSpeedCheck = async (pageSpeedCheckData) => {
try {
const pageSpeedCheck = await new PageSpeedCheck({
...pageSpeedCheckData,
}).save();
return pageSpeedCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createPageSpeedCheck";
throw error;
}
};
const createPageSpeedChecks = async (pageSpeedChecks) => {
try {
await PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createPageSpeedCheck";
throw error;
}
};
/**
* Delete all PageSpeed checks for a monitor
* @async
* @param {string} monitorId
* @returns {number}
* @throws {Error}
*/
const deletePageSpeedChecksByMonitorId = async (monitorId) => {
try {
const result = await PageSpeedCheck.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deletePageSpeedChecksByMonitorId";
throw error;
class PageSpeedCheckModule {
constructor({ PageSpeedCheck }) {
this.PageSpeedCheck = PageSpeedCheck;
}
};
export { createPageSpeedCheck, createPageSpeedChecks, deletePageSpeedChecksByMonitorId };
createPageSpeedChecks = async (pageSpeedChecks) => {
try {
await this.PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createPageSpeedCheck";
throw error;
}
};
deletePageSpeedChecksByMonitorId = async (monitorId) => {
try {
const result = await this.PageSpeedCheck.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deletePageSpeedChecksByMonitorId";
throw error;
}
};
}
export default PageSpeedCheckModule;
+70 -76
View File
@@ -1,86 +1,80 @@
import UserModel from "../../models/User.js";
import RecoveryToken from "../../models/RecoveryToken.js";
import crypto from "crypto";
import serviceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "recoveryModule";
/**
* Request a recovery token
* @async
* @param {string} email
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const requestRecoveryToken = async (email) => {
try {
// Delete any existing tokens
await RecoveryToken.deleteMany({ email });
let recoveryToken = new RecoveryToken({
email,
token: crypto.randomBytes(32).toString("hex"),
});
await recoveryToken.save();
return recoveryToken;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "requestRecoveryToken";
throw error;
class RecoveryModule {
constructor({ User, RecoveryToken, crypto, stringService }) {
this.User = User;
this.RecoveryToken = RecoveryToken;
this.crypto = crypto;
this.stringService = stringService;
}
};
const validateRecoveryToken = async (candidateToken) => {
const stringService = serviceRegistry.get(StringService.SERVICE_NAME);
try {
const recoveryToken = await RecoveryToken.findOne({
token: candidateToken,
});
if (recoveryToken !== null) {
requestRecoveryToken = async (email) => {
try {
// Delete any existing tokens
await this.RecoveryToken.deleteMany({ email });
let recoveryToken = new this.RecoveryToken({
email,
token: this.crypto.randomBytes(32).toString("hex"),
});
await recoveryToken.save();
return recoveryToken;
} else {
throw new Error(stringService.dbTokenNotFound);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "requestRecoveryToken";
throw error;
}
} catch (error) {
error.service = SERVICE_NAME;
error.method = "validateRecoveryToken";
throw error;
}
};
const resetPassword = async (password, candidateToken) => {
const stringService = serviceRegistry.get(StringService.SERVICE_NAME);
try {
const newPassword = password;
// Validate token again
const recoveryToken = await validateRecoveryToken(candidateToken);
const user = await UserModel.findOne({ email: recoveryToken.email });
if (user === null) {
throw new Error(stringService.dbUserNotFound);
};
validateRecoveryToken = async (candidateToken) => {
try {
const recoveryToken = await this.RecoveryToken.findOne({
token: candidateToken,
});
if (recoveryToken !== null) {
return recoveryToken;
} else {
throw new Error(this.stringService.dbTokenNotFound);
}
} catch (error) {
error.service = SERVICE_NAME;
error.method = "validateRecoveryToken";
throw error;
}
};
const match = await user.comparePassword(newPassword);
if (match === true) {
throw new Error(stringService.dbResetPasswordBadMatch);
resetPassword = async (password, candidateToken) => {
try {
const newPassword = password;
// Validate token again
const recoveryToken = await this.validateRecoveryToken(candidateToken);
const user = await this.User.findOne({ email: recoveryToken.email });
if (user === null) {
throw new Error(this.stringService.dbUserNotFound);
}
const match = await user.comparePassword(newPassword);
if (match === true) {
throw new Error("Password cannot be the same as the old password");
}
user.password = newPassword;
await user.save();
await this.RecoveryToken.deleteMany({ email: recoveryToken.email });
// Fetch the user again without the password
const userWithoutPassword = await this.User.findOne({
email: recoveryToken.email,
})
.select("-password")
.select("-profileImage");
return userWithoutPassword;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "resetPassword";
throw error;
}
};
}
user.password = newPassword;
await user.save();
await RecoveryToken.deleteMany({ email: recoveryToken.email });
// Fetch the user again without the password
const userWithoutPassword = await UserModel.findOne({
email: recoveryToken.email,
})
.select("-password")
.select("-profileImage");
return userWithoutPassword;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "resetPassword";
throw error;
}
};
export { requestRecoveryToken, validateRecoveryToken, resetPassword };
export default RecoveryModule;
+29 -34
View File
@@ -1,41 +1,36 @@
import AppSettings from "../../models/AppSettings.js";
// import AppSettings from "../../models/AppSettings.js";
const SERVICE_NAME = "SettingsModule";
const getAppSettings = async () => {
try {
const settings = AppSettings.findOne();
return settings;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getSettings";
throw error;
class SettingsModule {
constructor({ AppSettings }) {
this.AppSettings = AppSettings;
}
};
const updateAppSettings = async (newSettings) => {
try {
const update = { $set: { ...newSettings } };
updateAppSettings = async (newSettings) => {
try {
const update = { $set: { ...newSettings } };
if (newSettings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
if (newSettings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
}
if (newSettings.systemEmailPassword === "") {
update.$unset = { systemEmailPassword: "" };
delete update.$set.systemEmailPassword;
}
await this.AppSettings.findOneAndUpdate({}, update, {
upsert: true,
});
const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean();
return settings;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateAppSettings";
throw error;
}
};
}
if (newSettings.systemEmailPassword === "") {
update.$unset = { systemEmailPassword: "" };
delete update.$set.systemEmailPassword;
}
await AppSettings.findOneAndUpdate({}, update, {
upsert: true,
});
const settings = await AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean();
return settings;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateAppSettings";
throw error;
}
};
export { getAppSettings, updateAppSettings };
export default SettingsModule;
+268 -273
View File
@@ -1,292 +1,287 @@
import StatusPage from "../../models/StatusPage.js";
import { NormalizeData } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
// import StatusPage from "../../models/StatusPage.js";
// import { NormalizeData } from "../../../utils/dataUtils.js";
// import ServiceRegistry from "../../../service/system/serviceRegistry.js";
// import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "statusPageModule";
const createStatusPage = async ({ statusPageData, image, userId, teamId }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const statusPage = new StatusPage({
...statusPageData,
userId,
teamId,
});
if (image) {
statusPage.logo = {
data: image.buffer,
contentType: image.mimetype,
};
}
await statusPage.save();
return statusPage;
} catch (error) {
if (error?.code === 11000) {
// Handle duplicate URL errors
error.status = 400;
error.message = stringService.statusPageUrlNotUnique;
}
error.service = SERVICE_NAME;
error.method = "createStatusPage";
throw error;
class StatusPageModule {
constructor({ StatusPage, NormalizeData, stringService }) {
this.StatusPage = StatusPage;
this.NormalizeData = NormalizeData;
this.stringService = stringService;
}
};
const updateStatusPage = async (statusPageData, image) => {
try {
if (image) {
statusPageData.logo = {
data: image.buffer,
contentType: image.mimetype,
};
} else {
statusPageData.logo = null;
}
if (statusPageData.deleteSubmonitors === "true") {
statusPageData.subMonitors = [];
}
const statusPage = await StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, {
new: true,
});
return statusPage;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateStatusPage";
throw error;
}
};
const getStatusPageByUrl = async (url, type) => {
// TODO This is deprecated, can remove and have controller call getStatusPage
try {
return getStatusPage(url);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const getStatusPagesByTeamId = async (teamId) => {
try {
const statusPages = await StatusPage.find({ teamId });
return statusPages;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPagesByTeamId";
throw error;
}
};
const getStatusPage = async (url) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const preliminaryStatusPage = await StatusPage.findOne({ url });
if (!preliminaryStatusPage) {
const error = new Error(stringService.statusPageNotFound);
error.status = 404;
createStatusPage = async ({ statusPageData, image, userId, teamId }) => {
try {
const statusPage = new this.StatusPage({
...statusPageData,
userId,
teamId,
});
if (image) {
statusPage.logo = {
data: image.buffer,
contentType: image.mimetype,
};
}
await statusPage.save();
return statusPage;
} catch (error) {
if (error?.code === 11000) {
// Handle duplicate URL errors
error.status = 400;
error.message = this.stringService.statusPageUrlNotUnique;
}
error.service = SERVICE_NAME;
error.method = "createStatusPage";
throw error;
}
};
if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) {
const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } =
preliminaryStatusPage;
return {
statusPage: {
_id,
color,
companyName,
isPublished,
logo,
originalMonitors,
showCharts,
showUptimePercentage,
timezone,
showAdminLoginLink,
url,
},
monitors: [],
};
updateStatusPage = async (statusPageData, image) => {
try {
if (image) {
statusPageData.logo = {
data: image.buffer,
contentType: image.mimetype,
};
} else {
statusPageData.logo = null;
}
if (statusPageData.deleteSubmonitors === "true") {
statusPageData.subMonitors = [];
}
const statusPage = await this.StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, {
new: true,
});
return statusPage;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateStatusPage";
throw error;
}
};
const statusPageQuery = await StatusPage.aggregate([
{ $match: { url: url } },
{
$set: {
originalMonitors: "$monitors",
},
},
{
$lookup: {
from: "monitors",
localField: "monitors",
foreignField: "_id",
as: "monitors",
},
},
{
$unwind: {
path: "$monitors",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "maintenancewindows",
let: { monitorId: "$monitors._id" },
pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }],
as: "monitors.maintenanceWindows",
},
},
{
$lookup: {
from: "checks",
let: { monitorId: "$monitors._id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: 25 },
],
as: "monitors.checks",
},
},
{
$addFields: {
"monitors.orderIndex": {
$indexOfArray: ["$originalMonitors", "$monitors._id"],
},
"monitors.isMaintenance": {
$reduce: {
input: "$monitors.maintenanceWindows",
initialValue: false,
in: {
$or: [
"$$value",
{
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
},
],
},
},
},
},
},
{ $match: { "monitors.orderIndex": { $ne: -1 } } },
{ $sort: { "monitors.orderIndex": 1 } },
getStatusPageByUrl = async (url) => {
// TODO This is deprecated, can remove and have controller call getStatusPage
try {
return this.getStatusPage(url);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
{
$group: {
_id: "$_id",
statusPage: { $first: "$$ROOT" },
monitors: { $push: "$monitors" },
},
},
{
$project: {
getStatusPagesByTeamId = async (teamId) => {
try {
const statusPages = await this.StatusPage.find({ teamId });
return statusPages;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPagesByTeamId";
throw error;
}
};
getStatusPage = async (url) => {
try {
const preliminaryStatusPage = await this.StatusPage.findOne({ url });
if (!preliminaryStatusPage) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
}
if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) {
const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } =
preliminaryStatusPage;
return {
statusPage: {
_id: 1,
color: 1,
companyName: 1,
isPublished: 1,
logo: 1,
originalMonitors: 1,
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
showAdminLoginLink: 1,
url: 1,
_id,
color,
companyName,
isPublished,
logo,
originalMonitors,
showCharts,
showUptimePercentage,
timezone,
showAdminLoginLink,
url,
},
monitors: {
_id: 1,
userId: 1,
teamId: 1,
name: 1,
description: 1,
status: 1,
type: 1,
ignoreTlsErrors: 1,
jsonPath: 1,
expectedValue: 1,
matchMethod: 1,
url: 1,
port: 1,
isActive: 1,
interval: 1,
uptimePercentage: 1,
notifications: 1,
secret: 1,
thresholds: 1,
alertThreshold: 1,
cpuAlertThreshold: 1,
memoryAlertThreshold: 1,
diskAlertThreshold: 1,
tempAlertThreshold: 1,
checks: 1,
isMaintenance: 1,
createdAt: 1,
updatedAt: 1,
monitors: [],
};
}
const statusPageQuery = await this.StatusPage.aggregate([
{ $match: { url: url } },
{
$set: {
originalMonitors: "$monitors",
},
},
},
]);
if (!statusPageQuery.length) {
const error = new Error(stringService.statusPageNotFound);
error.status = 404;
{
$lookup: {
from: "monitors",
localField: "monitors",
foreignField: "_id",
as: "monitors",
},
},
{
$unwind: {
path: "$monitors",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "maintenancewindows",
let: { monitorId: "$monitors._id" },
pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }],
as: "monitors.maintenanceWindows",
},
},
{
$lookup: {
from: "checks",
let: { monitorId: "$monitors._id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: 25 },
],
as: "monitors.checks",
},
},
{
$addFields: {
"monitors.orderIndex": {
$indexOfArray: ["$originalMonitors", "$monitors._id"],
},
"monitors.isMaintenance": {
$reduce: {
input: "$monitors.maintenanceWindows",
initialValue: false,
in: {
$or: [
"$$value",
{
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
},
],
},
},
},
},
},
{ $match: { "monitors.orderIndex": { $ne: -1 } } },
{ $sort: { "monitors.orderIndex": 1 } },
{
$group: {
_id: "$_id",
statusPage: { $first: "$$ROOT" },
monitors: { $push: "$monitors" },
},
},
{
$project: {
statusPage: {
_id: 1,
color: 1,
companyName: 1,
isPublished: 1,
logo: 1,
originalMonitors: 1,
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
showAdminLoginLink: 1,
url: 1,
},
monitors: {
_id: 1,
userId: 1,
teamId: 1,
name: 1,
description: 1,
status: 1,
type: 1,
ignoreTlsErrors: 1,
jsonPath: 1,
expectedValue: 1,
matchMethod: 1,
url: 1,
port: 1,
isActive: 1,
interval: 1,
uptimePercentage: 1,
notifications: 1,
secret: 1,
thresholds: 1,
alertThreshold: 1,
cpuAlertThreshold: 1,
memoryAlertThreshold: 1,
diskAlertThreshold: 1,
tempAlertThreshold: 1,
checks: 1,
isMaintenance: 1,
createdAt: 1,
updatedAt: 1,
},
},
},
]);
if (!statusPageQuery.length) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
}
const { statusPage, monitors } = statusPageQuery[0];
const normalizedMonitors = monitors.map((monitor) => {
return {
...monitor,
checks: this.NormalizeData(monitor.checks, 10, 100),
};
});
return { statusPage, monitors: normalizedMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const { statusPage, monitors } = statusPageQuery[0];
deleteStatusPage = async (url) => {
try {
await this.StatusPage.deleteOne({ url });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPage";
throw error;
}
};
deleteStatusPagesByMonitorId = async (monitorId) => {
try {
await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPageByMonitorId";
throw error;
}
};
}
const normalizedMonitors = monitors.map((monitor) => {
return {
...monitor,
checks: NormalizeData(monitor.checks, 10, 100),
};
});
return { statusPage, monitors: normalizedMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const deleteStatusPage = async (url) => {
try {
await StatusPage.deleteOne({ url });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPage";
throw error;
}
};
const deleteStatusPagesByMonitorId = async (monitorId) => {
try {
await StatusPage.deleteMany({ monitors: { $in: [monitorId] } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPageByMonitorId";
throw error;
}
};
export {
createStatusPage,
updateStatusPage,
getStatusPagesByTeamId,
getStatusPage,
getStatusPageByUrl,
deleteStatusPage,
deleteStatusPagesByMonitorId,
};
export default StatusPageModule;
+153 -243
View File
@@ -1,261 +1,171 @@
import UserModel from "../../models/User.js";
import TeamModel from "../../models/Team.js";
import { GenerateAvatarImage } from "../../../utils/imageProcessing.js";
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
import { ParseBoolean } from "../../../utils/utils.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "userModule";
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
const checkSuperadmin = async () => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
class UserModule {
constructor({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }) {
this.User = User;
this.Team = Team;
this.GenerateAvatarImage = GenerateAvatarImage;
this.ParseBoolean = ParseBoolean;
this.stringService = stringService;
}
return false;
};
/**
* Insert a User
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const insertUser = async (userData, imageFile, generateAvatarImage = GenerateAvatarImage) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
if (imageFile) {
// 1. Save the full size image
userData.profileImage = {
data: imageFile.buffer,
contentType: imageFile.mimetype,
};
checkSuperadmin = async () => {
try {
const superAdmin = await this.User.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "checkSuperadmin";
throw error;
}
};
// 2. Get the avatar sized image
const avatar = await generateAvatarImage(imageFile);
userData.avatarImage = avatar;
insertUser = async (userData, imageFile) => {
try {
if (imageFile) {
// 1. Save the full size image
userData.profileImage = {
data: imageFile.buffer,
contentType: imageFile.mimetype,
};
// 2. Get the avatar sized image
const avatar = await this.GenerateAvatarImage(imageFile);
userData.avatarImage = avatar;
}
// Handle creating team if superadmin
if (userData.role.includes("superadmin")) {
const team = new this.Team({
email: userData.email,
});
userData.teamId = team._id;
userData.checkTTL = 60 * 60 * 24 * 30;
await team.save();
}
const newUser = new this.User(userData);
await newUser.save();
return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
error.message = this.stringService.dbUserExists;
}
error.service = SERVICE_NAME;
error.method = "insertUser";
throw error;
}
};
getUserByEmail = async (email) => {
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returning the user
const user = await this.User.findOne({ email: email }).select("-profileImage");
if (!user) {
throw new Error(this.stringService.dbUserNotFound);
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserByEmail";
throw error;
}
};
updateUser = async ({ userId, user, file }) => {
if (!userId) {
throw new Error("No user in request");
}
// Handle creating team if superadmin
if (userData.role.includes("superadmin")) {
const team = new TeamModel({
email: userData.email,
});
userData.teamId = team._id;
userData.checkTTL = 60 * 60 * 24 * 30;
await team.save();
try {
const candidateUser = { ...user };
if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) {
candidateUser.profileImage = null;
candidateUser.avatarImage = null;
} else if (file) {
// 1. Save the full size image
candidateUser.profileImage = {
data: file.buffer,
contentType: file.mimetype,
};
// 2. Get the avatar sized image
const avatar = await this.GenerateAvatarImage(file);
candidateUser.avatarImage = avatar;
}
const updatedUser = await this.User.findByIdAndUpdate(
userId,
candidateUser,
{ new: true } // Returns updated user instead of pre-update user
)
.select("-password")
.select("-profileImage");
return updatedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateUser";
throw error;
}
const newUser = new UserModel(userData);
await newUser.save();
return await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
error.message = stringService.dbUserExists;
};
deleteUser = async (userId) => {
try {
const deletedUser = await this.User.findByIdAndDelete(userId);
if (!deletedUser) {
throw new Error(this.stringService.dbUserNotFound);
}
return deletedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteUser";
throw error;
}
error.service = SERVICE_NAME;
error.method = "insertUser";
throw error;
}
};
};
/**
* Get User by Email
* Gets a user by Email. Not sure if we'll ever need this except for login.
* If not needed except for login, we can move password comparison here
* Throws error if user not found
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const getUserByEmail = async (email) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returning the user
const user = await UserModel.findOne({ email: email }).select("-profileImage");
if (!user) {
throw new Error(stringService.dbUserNotFound);
getAllUsers = async () => {
try {
const users = await this.User.find().select("-password").select("-profileImage");
return users;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllUsers";
throw error;
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserByEmail";
throw error;
}
};
};
/**
* Update a user by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
getUserById = async (roles, userId) => {
try {
if (!roles.includes("superadmin")) {
throw new Error("User is not a superadmin");
}
const updateUser = async ({ userId, user, file }) => {
if (!userId) {
throw new Error("No user in request");
}
const user = await this.User.findById(userId).select("-password").select("-profileImage");
if (!user) {
throw new Error("User not found");
}
try {
const candidateUser = { ...user };
if (ParseBoolean(candidateUser.deleteProfileImage) === true) {
candidateUser.profileImage = null;
candidateUser.avatarImage = null;
} else if (file) {
// 1. Save the full size image
candidateUser.profileImage = {
data: file.buffer,
contentType: file.mimetype,
};
// 2. Get the avatar sized image
const avatar = await GenerateAvatarImage(file);
candidateUser.avatarImage = avatar;
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserById";
throw error;
}
};
// ******************************************
// End handling profile image
// ******************************************
const updatedUser = await UserModel.findByIdAndUpdate(
userId,
candidateUser,
{ new: true } // Returns updated user instead of pre-update user
)
.select("-password")
.select("-profileImage");
return updatedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateUser";
throw error;
}
};
/**
* Delete a user by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const deleteUser = async (userId) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const deletedUser = await UserModel.findByIdAndDelete(userId);
if (!deletedUser) {
throw new Error(stringService.dbUserNotFound);
editUserById = async (userId, user) => {
try {
await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage");
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editUserById";
throw error;
}
return deletedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteUser";
throw error;
}
};
};
}
/**
* Delete a user by ID
* @async
* @param {string} teamId
* @returns {void}
* @throws {Error}
*/
const deleteTeam = async (teamId) => {
try {
await TeamModel.findByIdAndDelete(teamId);
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteTeam";
throw error;
}
};
const deleteAllOtherUsers = async () => {
try {
await UserModel.deleteMany({ role: { $ne: "superadmin" } });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteAllOtherUsers";
throw error;
}
};
const getAllUsers = async () => {
try {
const users = await UserModel.find().select("-password").select("-profileImage");
return users;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllUsers";
throw error;
}
};
const logoutUser = async (userId) => {
try {
await UserModel.updateOne({ _id: userId }, { $unset: { authToken: null } });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "logoutUser";
throw error;
}
};
const getUserById = async (roles, userId) => {
try {
if (!roles.includes("superadmin")) {
throw new Error("User is not a superadmin");
}
const user = await UserModel.findById(userId).select("-password").select("-profileImage");
if (!user) {
throw new Error("User not found");
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserById";
throw error;
}
};
const editUserById = async (userId, user) => {
try {
await UserModel.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage");
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editUserById";
throw error;
}
};
export {
checkSuperadmin,
insertUser,
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
getUserById,
editUserById,
};
export default UserModule;
+1 -1
View File
@@ -1,6 +1,6 @@
import Monitor from "../../models/Monitor.js";
import Check from "../../models/Check.js";
import logger from "../../../utils/logger.js";
import { logger } from "../../../utils/logger.js";
const generateRandomUrl = () => {
const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"];
+10 -5
View File
@@ -2,15 +2,16 @@ import { initializeServices } from "./config/services.js";
import { initializeControllers } from "./config/controllers.js";
import { createApp } from "./app.js";
import { initShutdownListener } from "./shutdown.js";
import logger from "./utils/logger.js";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
import Logger from "./utils/logger.js";
import SettingsService from "./service/system/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
const SERVICE_NAME = "Server";
let logger;
const startApp = async () => {
// FE path
@@ -18,12 +19,16 @@ const startApp = async () => {
const __dirname = path.dirname(__filename);
const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8"));
const frontendPath = path.join(__dirname, "public");
// Create services
const settingsService = new SettingsService(AppSettings);
const appSettings = settingsService.loadSettings();
const envSettings = settingsService.loadSettings();
// Create logger
logger = new Logger({ envSettings });
// Initialize services
const services = await initializeServices(appSettings, settingsService);
const services = await initializeServices({ logger, envSettings, settingsService });
// Initialize controllers
const controllers = initializeControllers(services);
@@ -31,12 +36,12 @@ const startApp = async () => {
const app = createApp({
services,
controllers,
appSettings,
envSettings,
frontendPath,
openApiSpec,
});
const port = appSettings.port || 52345;
const port = envSettings.port || 52345;
const server = app.listen(port, () => {
logger.info({ message: `Server started on port:${port}` });
});
+1 -1
View File
@@ -1,4 +1,4 @@
import logger from "../utils/logger.js";
import { logger } from "../utils/logger.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import logger from "../utils/logger.js";
import { logger } from "../utils/logger.js";
const languageMiddleware = (stringService, translationService) => async (req, res, next) => {
try {
+17
View File
@@ -0,0 +1,17 @@
import rateLimit from "express-rate-limit";
export const generalApiLimiter = rateLimit({
window: 60 * 1000,
limit: 600,
standardHeaders: true,
legacyHeaders: false,
ipv6Subnet: 64,
});
export const authApiLimiter = rateLimit({
window: 60 * 1000,
limit: 15,
standardHeaders: true,
legacyHeaders: false,
ipv6Subnet: 64,
});
+1 -1
View File
@@ -1,4 +1,4 @@
import logger from "../utils/logger.js";
import { logger } from "../utils/logger.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
import { ObjectId } from "mongodb";
+11 -11
View File
@@ -23,7 +23,7 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor) {
throw this.errorService.createNotFoundError("Monitor not found");
@@ -34,7 +34,7 @@ class CheckService {
}
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
const result = await this.db.getChecksByMonitor({
const result = await this.db.checkModule.getChecksByMonitor({
monitorId,
type,
sortOrder,
@@ -55,7 +55,7 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const checkData = await this.db.getChecksByTeam({
const checkData = await this.db.checkModule.getChecksByTeam({
sortOrder,
dateRange,
filter,
@@ -72,7 +72,7 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
const summary = await this.db.checkModule.getChecksSummaryByTeamId({ teamId });
return summary;
};
@@ -85,7 +85,7 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
const updatedCheck = await this.db.checkModule.ackCheck(checkId, teamId, ack);
return updatedCheck;
};
@@ -95,7 +95,7 @@ class CheckService {
throw this.errorService.createBadRequestError("No monitor ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor) {
throw this.errorService.createNotFoundError("Monitor not found");
}
@@ -105,7 +105,7 @@ class CheckService {
}
}
const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path);
const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path);
return updatedChecks;
};
@@ -118,7 +118,7 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor) {
throw this.errorService.createNotFoundError("Monitor not found");
@@ -128,7 +128,7 @@ class CheckService {
throw this.errorService.createAuthorizationError();
}
const deletedCount = await this.db.deleteChecks(monitorId);
const deletedCount = await this.db.checkModule.deleteChecks(monitorId);
return deletedCount;
};
deleteChecksByTeamId = async ({ teamId }) => {
@@ -136,14 +136,14 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const deletedCount = await this.db.deleteChecksByTeamId(teamId);
const deletedCount = await this.db.checkModule.deleteChecksByTeamId(teamId);
return deletedCount;
};
updateChecksTTL = async ({ teamId, ttl }) => {
const SECONDS_PER_DAY = 86400;
const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY;
await this.db.updateChecksTTL(teamId, newTTL);
await this.db.checkModule.updateChecksTTL(teamId, newTTL);
};
}
+3 -3
View File
@@ -17,12 +17,12 @@ class InviteService {
getInviteToken = async ({ invite, teamId }) => {
invite.teamId = teamId;
const inviteToken = await this.db.requestInviteToken(invite);
const inviteToken = await this.db.inviteModule.requestInviteToken(invite);
return inviteToken;
};
sendInviteEmail = async ({ inviteRequest, firstName }) => {
const inviteToken = await this.db.requestInviteToken({ ...inviteRequest });
const inviteToken = await this.db.inviteModule.requestInviteToken({ ...inviteRequest });
const { clientHost } = this.settingsService.getSettings();
const html = await this.emailService.buildEmail("employeeActivationTemplate", {
@@ -36,7 +36,7 @@ class InviteService {
};
verifyInviteToken = async ({ inviteToken }) => {
const invite = await this.db.getInviteToken(inviteToken);
const invite = await this.db.inviteModule.getInviteToken(inviteToken);
return invite;
};
}
@@ -16,7 +16,7 @@ class MaintenanceWindowService {
createMaintenanceWindow = async ({ teamId, body }) => {
const monitorIds = body.monitors;
const monitors = await this.db.getMonitorsByIds(monitorIds);
const monitors = await this.db.monitorModule.getMonitorsByIds(monitorIds);
const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId));
@@ -25,7 +25,7 @@ class MaintenanceWindowService {
}
const dbTransactions = monitorIds.map((monitorId) => {
return this.db.createMaintenanceWindow({
return this.db.maintenanceWindowModule.createMaintenanceWindow({
teamId,
monitorId,
name: body.name,
@@ -39,26 +39,26 @@ class MaintenanceWindowService {
};
getMaintenanceWindowById = async ({ id, teamId }) => {
const maintenanceWindow = await this.db.getMaintenanceWindowById({ id, teamId });
const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId });
return maintenanceWindow;
};
getMaintenanceWindowsByTeamId = async ({ teamId, query }) => {
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(teamId, query);
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query);
return maintenanceWindows;
};
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
return maintenanceWindows;
};
deleteMaintenanceWindow = async ({ id, teamId }) => {
await this.db.deleteMaintenanceWindowById({ id, teamId });
await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId });
};
editMaintenanceWindow = async ({ id, teamId, body }) => {
const editedMaintenanceWindow = await this.db.editMaintenanceWindowById({ id, body, teamId });
const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId });
return editedMaintenanceWindow;
};
}
+21 -21
View File
@@ -20,20 +20,20 @@ class MonitorService {
}
verifyTeamAccess = async ({ teamId, monitorId }) => {
const monitor = await this.db.getMonitorById(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.getAllMonitors();
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.getUptimeDetailsById({
const data = await this.db.monitorModule.getUptimeDetailsById({
monitorId,
dateRange,
normalize,
@@ -44,7 +44,7 @@ class MonitorService {
getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitorStats = await this.db.getMonitorStatsById({
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
monitorId,
limit,
sortOrder,
@@ -58,20 +58,20 @@ class MonitorService {
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
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.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
return monitor;
};
createMonitor = async ({ teamId, userId, body }) => {
const monitor = await this.db.createMonitor({
const monitor = await this.db.monitorModule.createMonitor({
body,
teamId,
userId,
@@ -122,7 +122,7 @@ class MonitorService {
await createMonitorsBodyValidation.validateAsync(enrichedData);
const monitors = await this.db.createBulkMonitors(enrichedData);
const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData);
await Promise.all(
monitors.map(async (monitor) => {
@@ -141,21 +141,21 @@ class MonitorService {
deleteMonitor = async ({ teamId, monitorId }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.deleteMonitor({ teamId, monitorId });
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
await this.jobQueue.deleteJob(monitor);
await this.db.deleteStatusPagesByMonitorId(monitor._id);
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
return monitor;
};
deleteAllMonitors = async ({ teamId }) => {
const { monitors, deletedCount } = await this.db.deleteAllMonitors(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.deleteChecks(monitor._id);
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
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}`,
@@ -171,19 +171,19 @@ class MonitorService {
editMonitor = async ({ teamId, monitorId, body }) => {
await this.verifyTeamAccess({ teamId, monitorId });
const editedMonitor = await this.db.editMonitor({ monitorId, body });
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.pauseMonitor({ 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.addDemoMonitors(userId, teamId);
const demoMonitors = await this.db.monitorModuleaddDemoMonitors(userId, teamId);
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
return demoMonitors;
};
@@ -203,7 +203,7 @@ class MonitorService {
};
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
const monitors = await this.db.getMonitorsByTeamId({
const monitors = await this.db.monitorModule.getMonitorsByTeamId({
limit,
type,
page,
@@ -217,7 +217,7 @@ class MonitorService {
};
getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => {
const result = await this.db.getMonitorsAndSummaryByTeamId({
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
type,
explain,
teamId,
@@ -226,7 +226,7 @@ class MonitorService {
};
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
const result = await this.db.getMonitorsWithChecksByTeamId({
const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({
limit,
type,
page,
@@ -241,7 +241,7 @@ class MonitorService {
};
exportMonitorsToCSV = async ({ teamId }) => {
const monitors = await this.db.getMonitorsByTeamId({ teamId });
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
if (!monitors || monitors.length === 0) {
throw this.errorService.createNotFoundError("No monitors to export");
+18 -18
View File
@@ -29,18 +29,18 @@ class UserService {
registerUser = async (user, file) => {
// Create a new user
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await this.db.checkSuperadmin();
const superAdminExists = await this.db.userModule.checkSuperadmin();
if (superAdminExists) {
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
user.teamId = invitedUser.teamId;
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto.randomBytes(64).toString("hex");
await this.db.updateAppSettings({ jwtSecret });
const jwtSecret = this.crypto.randomBytes(64).toString("hex");
await this.db.settingsModule.updateAppSettings({ jwtSecret });
}
const newUser = await this.db.insertUser({ ...user }, file);
const newUser = await this.db.userModule.insertUser({ ...user }, file);
this.logger.debug({
message: "New user created",
@@ -83,7 +83,7 @@ class UserService {
loginUser = async (email, password) => {
// Check if user exists
const user = await this.db.getUserByEmail(email);
const user = await this.db.userModule.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
@@ -110,7 +110,7 @@ class UserService {
// Add user email to body for DB operation
updates.email = currentUser.email;
// Get user
const user = await this.db.getUserByEmail(currentUser.email);
const user = await this.db.userModule.getUserByEmail(currentUser.email);
// Compare passwords
const match = await user.comparePassword(updates?.password);
// If not a match, throw a 403
@@ -122,18 +122,18 @@ class UserService {
updates.password = updates.newPassword;
}
const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file });
const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file });
return updatedUser;
};
checkSuperadminExists = async () => {
const superAdminExists = await this.db.checkSuperadmin();
const superAdminExists = await this.db.userModule.checkSuperadmin();
return superAdminExists;
};
requestRecovery = async (email) => {
const user = await this.db.getUserByEmail(email);
const recoveryToken = await this.db.requestRecoveryToken(email);
const user = await this.db.userModule.getUserByEmail(email);
const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email);
const name = user.firstName;
const { clientHost } = this.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
@@ -148,11 +148,11 @@ class UserService {
};
validateRecovery = async (recoveryToken) => {
await this.db.validateRecoveryToken(recoveryToken);
await this.db.recoveryModule.validateRecoveryToken(recoveryToken);
};
resetPassword = async (password, recoveryToken) => {
const user = await this.db.resetPassword(password, recoveryToken);
const user = await this.db.recoveryModule.resetPassword(password, recoveryToken);
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(user._doc, appSettings);
return { user, token };
@@ -181,7 +181,7 @@ class UserService {
}
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this.db.getMonitorsByTeamId({
const result = await this.db.monitorModule.getMonitorsByTeamId({
teamId: teamId,
});
@@ -195,21 +195,21 @@ class UserService {
));
}
// 6. Delete the user by id
await this.db.deleteUser(userId);
await this.db.userModule.deleteUser(userId);
};
getAllUsers = async () => {
const users = await this.db.getAllUsers();
const users = await this.db.userModule.getAllUsers();
return users;
};
getUserById = async (roles, userId) => {
const user = await this.db.getUserById(roles, userId);
const user = await this.db.userModule.getUserById(roles, userId);
return user;
};
editUserById = async (userId, user) => {
await this.db.editUserById(userId, user);
await this.db.userModule.editUserById(userId, user);
};
}
export default UserService;
-64
View File
@@ -1,64 +0,0 @@
const SERVICE_NAME = "RedisService";
class RedisService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ Redis, logger }) {
this.Redis = Redis;
this.connections = new Set();
this.logger = logger;
}
get serviceName() {
return RedisService.SERVICE_NAME;
}
getNewConnection(options = {}) {
const connection = new this.Redis(process.env.REDIS_URL, {
retryStrategy: (times) => {
return null;
},
...options,
});
this.connections.add(connection);
return connection;
}
async closeAllConnections() {
const closePromises = Array.from(this.connections).map((conn) =>
conn.quit().catch((err) => {
this.logger.error({
message: "Error closing Redis connection",
service: SERVICE_NAME,
method: "closeAllConnections",
details: { error: err },
});
})
);
await Promise.all(closePromises);
this.connections.clear();
this.logger.info({
message: "All Redis connections closed",
service: SERVICE_NAME,
method: "closeAllConnections",
});
}
async flushRedis() {
this.logger.info({
message: "Flushing Redis",
service: SERVICE_NAME,
method: "flushRedis",
});
const flushPromises = Array.from(this.connections).map((conn) => conn.flushall());
await Promise.all(flushPromises);
this.logger.info({
message: "Redis flushed",
service: SERVICE_NAME,
method: "flushRedis",
});
}
}
export default RedisService;
@@ -1,321 +0,0 @@
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"];
const SERVICE_NAME = "JobQueue";
const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
const QUEUE_LOOKUP = {
hardware: "hardware",
http: "uptime",
ping: "uptime",
port: "uptime",
docker: "uptime",
pagespeed: "pagespeed",
};
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
class JobQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, jobQueueHelper, logger, stringService }) {
this.db = db;
this.jobQueueHelper = jobQueueHelper;
this.stringService = stringService;
this.logger = logger;
this.queues = {};
this.workers = [];
}
get serviceName() {
return JobQueue.SERVICE_NAME;
}
static async create({ db, jobQueueHelper, logger, stringService }) {
const instance = new JobQueue({ db, jobQueueHelper, logger, stringService });
await instance.init();
return instance;
}
async init() {
try {
await this.initQueues();
await this.initWorkers();
const monitors = await this.db.getAllMonitors();
await Promise.all(
monitors
.filter((monitor) => monitor.isActive)
.map(async (monitor) => {
try {
await this.addJob(monitor._id, monitor);
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "initJobQueue",
stack: error.stack,
});
}
})
);
this.healthCheckInterval = setInterval(async () => {
try {
const queueHealthChecks = await this.checkQueueHealth();
const queueIsStuck = queueHealthChecks.some((healthCheck) => healthCheck.stuck);
if (queueIsStuck) {
this.logger.warn({
message: "Queue is stuck",
service: SERVICE_NAME,
method: "periodicHealthCheck",
details: queueHealthChecks,
});
await this.flushQueues();
}
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "periodicHealthCheck",
stack: error.stack,
});
}
}, HEALTH_CHECK_INTERVAL);
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "initJobQueue",
stack: error.stack,
});
}
}
async initQueues() {
const readyPromises = [];
for (const queueName of QUEUE_NAMES) {
const q = this.jobQueueHelper.createQueue(queueName);
this.queues[queueName] = q;
readyPromises.push(q.waitUntilReady());
}
await Promise.all(readyPromises);
this.logger.info({
message: "Queues ready",
service: SERVICE_NAME,
method: "initQueues",
});
}
async initWorkers() {
const workerReadyPromises = [];
for (const queueName of QUEUE_NAMES) {
const worker = this.jobQueueHelper.createWorker(queueName, this.queues[queueName]);
this.workers.push(worker);
workerReadyPromises.push(worker.waitUntilReady());
}
await Promise.all(workerReadyPromises);
this.logger.info({
message: "Workers ready",
service: SERVICE_NAME,
method: "initWorkers",
});
}
pauseJob = async (monitor) => {
this.deleteJob(monitor);
};
resumeJob = async (monitor) => {
this.addJob(monitor._id, monitor);
};
async addJob(jobName, monitor) {
this.logger.info({
message: `Adding job ${monitor?.url ?? "No URL"}`,
service: SERVICE_NAME,
method: "addJob",
});
const queueName = QUEUE_LOOKUP[monitor.type];
const queue = this.queues[queueName];
if (typeof queue === "undefined") {
throw new Error(`Queue for ${monitor.type} not found`);
}
const jobTemplate = {
name: jobName,
data: monitor,
opts: {
attempts: 1,
backoff: {
type: "exponential",
delay: 1000,
},
removeOnComplete: true,
removeOnFail: false,
timeout: 1 * 60 * 1000,
},
};
const schedulerId = getSchedulerId(monitor);
await queue.upsertJobScheduler(schedulerId, { every: monitor?.interval ?? 60000 }, jobTemplate);
}
async deleteJob(monitor) {
try {
const queue = this.queues[QUEUE_LOOKUP[monitor.type]];
const schedulerId = getSchedulerId(monitor);
const wasDeleted = await queue.removeJobScheduler(schedulerId);
if (wasDeleted === true) {
this.logger.info({
message: this.stringService.jobQueueDeleteJob,
service: SERVICE_NAME,
method: "deleteJob",
details: `Deleted job ${monitor._id}`,
});
return true;
} else {
this.logger.error({
message: this.stringService.jobQueueDeleteJob,
service: SERVICE_NAME,
method: "deleteJob",
details: `Failed to delete job ${monitor._id}`,
});
return false;
}
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteJob") : null;
throw error;
}
}
async updateJob(monitor) {
await this.deleteJob(monitor);
await this.addJob(monitor._id, monitor);
}
async getJobs() {
try {
let stats = {};
await Promise.all(
QUEUE_NAMES.map(async (name) => {
const queue = this.queues[name];
const jobs = await queue.getJobs();
const ret = await Promise.all(
jobs.map(async (job) => {
const state = await job.getState();
return { url: job.data.url, state, progress: job.progress };
})
);
stats[name] = { jobs: ret };
})
);
return stats;
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getJobStats") : null;
throw error;
}
}
async getMetrics() {
try {
let metrics = {};
await Promise.all(
QUEUE_NAMES.map(async (name) => {
const queue = this.queues[name];
const [waiting, active, failed, delayed, repeatableJobs] = await Promise.all([
queue.getWaitingCount(),
queue.getActiveCount(),
queue.getFailedCount(),
queue.getDelayedCount(),
queue.getRepeatableJobs(),
]);
metrics[name] = {
waiting,
active,
failed,
delayed,
repeatableJobs: repeatableJobs.length,
};
})
);
return metrics;
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getMetrics",
stack: error.stack,
});
}
}
async checkQueueHealth() {
const res = [];
for (const queueName of QUEUE_NAMES) {
const q = this.queues[queueName];
await q.waitUntilReady();
const lastJobProcessedTime = q.lastJobProcessedTime;
const currentTime = Date.now();
const timeDiff = currentTime - lastJobProcessedTime;
// Check for jobs
const jobCounts = await q.getJobCounts();
const hasJobs = Object.values(jobCounts).some((count) => count > 0);
res.push({
queueName,
timeSinceLastJob: timeDiff,
stuck: hasJobs && timeDiff > 10000,
jobCounts,
});
}
return res;
}
async flushQueues() {
try {
this.logger.warn({
message: "Flushing queues",
method: "flushQueues",
service: SERVICE_NAME,
});
for (const worker of this.workers) {
await worker.close();
}
this.workers = [];
for (const queue of Object.values(this.queues)) {
await queue.obliterate();
}
this.queue = {};
await this.init();
return true;
} catch (error) {
this.logger.warn({
message: `${error.message} - Flushing redis manually`,
service: SERVICE_NAME,
method: "flushQueues",
});
return await this.jobQueueHelper.flushRedis();
}
}
async shutdown() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
for (const worker of this.workers) {
await worker.close();
}
for (const queue of Object.values(this.queues)) {
await queue.obliterate();
}
}
}
export default JobQueue;
@@ -1,308 +0,0 @@
const SERVICE_NAME = "JobQueueHelper";
class JobQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
constructor({ redisService, Queue, Worker, logger, db, networkService, statusService, notificationService }) {
this.db = db;
this.redisService = redisService;
this.Queue = Queue;
this.Worker = Worker;
this.logger = logger;
this.networkService = networkService;
this.statusService = statusService;
this.notificationService = notificationService;
}
get serviceName() {
return JobQueueHelper.SERVICE_NAME;
}
createQueue(queueName) {
const connection = this.redisService.getNewConnection();
const q = new this.Queue(queueName, {
connection,
});
q.lastJobProcessedTime = Date.now();
q.on("cleaned", (jobs, type) => {
this.logger.debug({
message: `Queue ${queueName} is cleaned with jobs: ${jobs} and type: ${type}`,
service: SERVICE_NAME,
method: "createQueue:cleaned",
});
});
q.on("error", (err) => {
this.logger.error({
message: `Queue ${queueName} is error with msg: ${err}`,
service: SERVICE_NAME,
method: "createQueue:error",
});
});
q.on("ioredis:close", () => {
this.logger.debug({
message: `Queue ${queueName} is ioredis:close`,
service: SERVICE_NAME,
method: "createQueue:ioredis:close",
});
});
q.on("paused", () => {
this.logger.debug({
message: `Queue ${queueName} is paused`,
service: SERVICE_NAME,
method: "createQueue:paused",
});
});
q.on("progress", (job, progress) => {
this.logger.debug({
message: `Queue ${queueName} is progress with msg: ${progress}`,
service: SERVICE_NAME,
method: "createQueue:progress",
});
});
q.on("removed", (job) => {
this.logger.debug({
message: `Queue ${queueName} is removed with msg: ${job}`,
service: SERVICE_NAME,
method: "createQueue:removed",
});
});
q.on("resumed", () => {
this.logger.debug({
message: `Queue ${queueName} is resumed`,
service: SERVICE_NAME,
method: "createQueue:resumed",
});
});
q.on("waiting", () => {
this.logger.debug({
message: `Queue ${queueName} is waiting`,
service: SERVICE_NAME,
method: "createQueue:waiting",
});
});
return q;
}
createWorker(queueName, queue) {
const connection = this.redisService.getNewConnection({
maxRetriesPerRequest: null,
});
const worker = new this.Worker(queueName, this.createJobHandler(queue), {
connection,
concurrency: 50,
});
worker.on("active", (job) => {
this.logger.debug({
message: `Worker ${queueName} is active`,
service: SERVICE_NAME,
method: "createWorker:active",
});
});
worker.on("closed", () => {
this.logger.debug({
message: `Worker ${queueName} is closed`,
service: SERVICE_NAME,
method: "createWorker:closed",
});
});
worker.on("closing", (msg) => {
this.logger.debug({
message: `Worker ${queueName} is closing with msg: ${msg}`,
service: SERVICE_NAME,
method: "createWorker:closing",
});
});
worker.on("completed", (job) => {
this.logger.debug({
message: `Worker ${queueName} is completed`,
service: SERVICE_NAME,
method: "createWorker:completed",
});
});
worker.on("drained", () => {
this.logger.debug({
message: `Worker ${queueName} is drained`,
service: SERVICE_NAME,
method: "createWorker:drained",
});
});
worker.on("error", (failedReason) => {
this.logger.error({
message: `Worker ${queueName} is error with msg: ${failedReason}`,
service: SERVICE_NAME,
method: "createWorker:error",
});
});
worker.on("failed", (job, error, prev) => {
this.logger.error({
message: `Worker ${queueName} is failed with msg: ${error.message}`,
service: error?.service ?? SERVICE_NAME,
method: error?.method ?? "createWorker:failed",
stack: error?.stack,
});
});
worker.on("ioredis:close", () => {
this.logger.debug({
message: `Worker ${queueName} is ioredis:close`,
service: SERVICE_NAME,
method: "createWorker:ioredis:close",
});
});
worker.on("paused", () => {
this.logger.debug({
message: `Worker ${queueName} is paused`,
service: SERVICE_NAME,
method: "createWorker:paused",
});
});
worker.on("progress", (job, progress) => {
this.logger.debug({
message: `Worker ${queueName} is progress with msg: ${progress}`,
service: SERVICE_NAME,
method: "createWorker:progress",
});
});
worker.on("ready", () => {
this.logger.debug({
message: `Worker ${queueName} is ready`,
service: SERVICE_NAME,
method: "createWorker:ready",
});
});
worker.on("resumed", () => {
this.logger.debug({
message: `Worker ${queueName} is resumed`,
service: SERVICE_NAME,
method: "createWorker:resumed",
});
});
worker.on("stalled", () => {
this.logger.warn({
message: `Worker ${queueName} is stalled`,
service: SERVICE_NAME,
method: "createWorker:stalled",
});
});
return worker;
}
async isInMaintenanceWindow(monitorId) {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
const now = new Date();
const repeatInterval = window.repeat || 0;
// If start is < now and end > now, we're in maintenance
if (start <= now && end >= now) return true;
// If maintenance window was set in the past with a repeat,
// we need to advance start and end to see if we are in range
while (start < now && repeatInterval !== 0) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
if (start <= now && end >= now) {
return true;
}
}
return false;
}
return acc;
}, false);
return maintenanceWindowIsActive;
}
createJobHandler(q) {
return async (job) => {
try {
// Update the last job processed time for this queue
q.lastJobProcessedTime = Date.now();
// Get all maintenance windows for this monitor
await job.updateProgress(0);
const monitorId = job.data._id;
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
// If a maintenance window is active, we're done
if (maintenanceWindowActive) {
await job.updateProgress(100);
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "createWorker",
});
return false;
}
// Get the current status
await job.updateProgress(30);
const monitor = job.data;
const networkResponse = await this.networkService.getStatus(monitor);
// If the network response is not found, we're done
if (!networkResponse) {
await job.updateProgress(100);
return false;
}
// Handle status change
await job.updateProgress(60);
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
// Handle notifications
await job.updateProgress(80);
this.notificationService
.handleNotifications({
...networkResponse,
monitor: updatedMonitor,
prevStatus,
statusChanged,
})
.catch((error) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "createJobHandler",
details: `Error sending notifications for job ${job.id}: ${error.message}`,
stack: error.stack,
});
});
await job.updateProgress(100);
return true;
} catch (error) {
await job.updateProgress(100);
throw error;
}
};
}
async flushRedis() {
try {
const connection = this.redisService.getNewConnection();
const flushResult = await connection.flushall();
return flushResult;
} catch (error) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "flushRedis",
});
return false;
}
}
}
export default JobQueueHelper;
@@ -1,213 +0,0 @@
import { Pulse } from "@pulsecron/pulse";
const SERVICE_NAME = "JobQueue";
class PulseQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor({ appSettings, db, pulseQueueHelper, logger }) {
this.db = db;
this.appSettings = appSettings;
this.pulseQueueHelper = pulseQueueHelper;
this.logger = logger;
}
get serviceName() {
return PulseQueue.SERVICE_NAME;
}
static async create({ appSettings, db, pulseQueueHelper, logger }) {
const instance = new PulseQueue({ appSettings, db, pulseQueueHelper, logger });
await instance.init();
return instance;
}
// ****************************************
// Core methods
// ****************************************
init = async () => {
try {
const mongoConnectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
this.pulse = new Pulse({ db: { address: mongoConnectionString } });
await this.pulse.start();
this.pulse.define("monitor-job", this.pulseQueueHelper.getMonitorJob(), {});
const monitors = await this.db.getAllMonitors();
for (const monitor of monitors) {
await this.addJob(monitor._id, monitor);
}
return true;
} catch (error) {
this.logger.error({
message: "Failed to initialize PulseQueue",
service: SERVICE_NAME,
method: "init",
details: error,
});
return false;
}
};
addJob = async (monitorId, monitor) => {
this.logger.debug({
message: `Adding job ${monitor?.url ?? "No URL"}`,
service: SERVICE_NAME,
method: "addJob",
});
const intervalInSeconds = monitor.interval / 1000;
const job = this.pulse.create("monitor-job", {
monitor,
});
job.unique({ "data.monitor._id": monitor._id });
job.attrs.jobId = monitorId.toString();
job.repeatEvery(`${intervalInSeconds} seconds`);
if (monitor.isActive === false) {
job.disable();
}
await job.save();
};
deleteJob = async (monitor) => {
this.logger.debug({
message: `Deleting job ${monitor?.url ?? "No URL"}`,
service: SERVICE_NAME,
method: "deleteJob",
});
await this.pulse.cancel({
"data.monitor._id": monitor._id,
});
};
pauseJob = async (monitor) => {
const result = await this.pulse.disable({
"data.monitor._id": monitor._id,
});
if (result.length < 1) {
throw new Error("Failed to pause monitor");
}
this.logger.debug({
message: `Paused monitor ${monitor._id}`,
service: SERVICE_NAME,
method: "pauseJob",
});
};
resumeJob = async (monitor) => {
const result = await this.pulse.enable({
"data.monitor._id": monitor._id,
});
if (result.length < 1) {
throw new Error("Failed to resume monitor");
}
this.logger.debug({
message: `Resumed monitor ${monitor._id}`,
service: SERVICE_NAME,
method: "resumeJob",
});
};
updateJob = async (monitor) => {
const jobs = await this.pulse.jobs({
"data.monitor._id": monitor._id,
});
const job = jobs[0];
if (!job) {
throw new Error("Job not found");
}
const intervalInSeconds = monitor.interval / 1000;
job.repeatEvery(`${intervalInSeconds} seconds`);
job.attrs.data.monitor = monitor;
await job.save();
};
shutdown = async () => {
this.logger.info({
message: "Shutting down JobQueue",
service: SERVICE_NAME,
method: "shutdown",
});
await this.pulse.stop();
};
// ****************************************
// Diagnostic methods
// ****************************************
getMetrics = async () => {
const jobs = await this.pulse.jobs();
const metrics = jobs.reduce(
(acc, job) => {
acc.totalRuns += job.attrs.runCount || 0;
acc.totalFailures += job.attrs.failCount || 0;
acc.jobs++;
if (job.attrs.failCount > 0 && job.attrs.failedAt >= job.attrs.lastFinishedAt) {
acc.failingJobs++;
}
if (job.attrs.lockedAt) {
acc.activeJobs++;
}
if (job.attrs.failCount > 0) {
acc.jobsWithFailures.push({
monitorId: job.attrs.data.monitor._id,
monitorUrl: job.attrs.data.monitor.url,
monitorType: job.attrs.data.monitor.type,
failedAt: job.attrs.failedAt,
failCount: job.attrs.failCount,
failReason: job.attrs.failReason,
});
}
return acc;
},
{
jobs: 0,
activeJobs: 0,
failingJobs: 0,
jobsWithFailures: [],
totalRuns: 0,
totalFailures: 0,
}
);
return metrics;
};
getJobs = async () => {
const jobs = await this.pulse.jobs();
return jobs.map((job) => {
return {
monitorId: job.attrs.data.monitor._id,
monitorUrl: job.attrs.data.monitor.url,
monitorType: job.attrs.data.monitor.type,
active: !job.attrs.disabled,
lockedAt: job.attrs.lockedAt,
runCount: job.attrs.runCount || 0,
failCount: job.attrs.failCount || 0,
failReason: job.attrs.failReason,
lastRunAt: job.attrs.lastRunAt,
lastFinishedAt: job.attrs.lastFinishedAt,
lastRunTook: job.attrs.lockedAt ? null : job.attrs.lastFinishedAt - job.attrs.lastRunAt,
lastFailedAt: job.attrs.failedAt,
};
});
};
flushQueues = async () => {
const cancelRes = await this.pulse.cancel();
await this.pulse.stop();
const initRes = await this.init();
return {
flushedJobs: cancelRes,
initSuccess: initRes,
};
};
obliterate = async () => {
await this.flushQueues();
};
}
export default PulseQueue;
@@ -1,104 +0,0 @@
const SERVICE_NAME = "PulseQueueHelper";
class PulseQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, logger, networkService, statusService, notificationService }) {
this.db = db;
this.logger = logger;
this.networkService = networkService;
this.statusService = statusService;
this.notificationService = notificationService;
}
get serviceName() {
return PulseQueueHelper.SERVICE_NAME;
}
getMonitorJob = () => {
return async (job) => {
try {
const monitor = job.attrs.data.monitor;
const monitorId = job.attrs.data.monitor._id;
if (!monitorId) {
throw new Error("No monitor id");
}
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
if (maintenanceWindowActive) {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "getMonitorJob",
});
return;
}
const networkResponse = await this.networkService.getStatus(monitor);
if (!networkResponse) {
throw new Error("No network response");
}
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
this.notificationService
.handleNotifications({
...networkResponse,
monitor: updatedMonitor,
prevStatus,
statusChanged,
})
.catch((error) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getMonitorJob",
details: `Error sending notifications for job ${job.id}: ${error.message}`,
stack: error.stack,
});
});
} catch (error) {
this.logger.warn({
message: error.message,
service: error.service || SERVICE_NAME,
method: error.method || "getMonitorJob",
stack: error.stack,
});
throw error;
}
};
};
async isInMaintenanceWindow(monitorId) {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
const now = new Date();
const repeatInterval = window.repeat || 0;
// If start is < now and end > now, we're in maintenance
if (start <= now && end >= now) return true;
// If maintenance window was set in the past with a repeat,
// we need to advance start and end to see if we are in range
while (start < now && repeatInterval !== 0) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
if (start <= now && end >= now) {
return true;
}
}
return false;
}
return acc;
}, false);
return maintenanceWindowIsActive;
}
}
export default PulseQueueHelper;
@@ -4,8 +4,8 @@ const SERVICE_NAME = "JobQueue";
class SuperSimpleQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor({ appSettings, db, logger, helper }) {
this.appSettings = appSettings;
constructor({ envSettings, db, logger, helper }) {
this.envSettings = envSettings;
this.db = db;
this.logger = logger;
this.helper = helper;
@@ -15,8 +15,8 @@ class SuperSimpleQueue {
return SuperSimpleQueue.SERVICE_NAME;
}
static async create({ appSettings, db, logger, helper }) {
const instance = new SuperSimpleQueue({ appSettings, db, logger, helper });
static async create({ envSettings, db, logger, helper }) {
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper });
await instance.init();
return instance;
}
@@ -28,12 +28,12 @@ class SuperSimpleQueue {
// storeType: "redis",
logLevel: "debug",
debug: true,
dbUri: this.appSettings.dbConnectionString,
// dbUri: this.envSettings.dbConnectionString,
});
this.scheduler.start();
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
const monitors = await this.db.getAllMonitors();
const monitors = await this.db.monitorModule.getAllMonitors();
for (const monitor of monitors) {
await this.addJob(monitor._id, monitor);
}
@@ -67,7 +67,7 @@ class SuperSimpleQueueHelper {
};
async isInMaintenanceWindow(monitorId) {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
@@ -1,5 +1,4 @@
const SERVICE_NAME = "BufferService";
const BUFFER_TIMEOUT = process.env.NODE_ENV === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
const TYPE_MAP = {
http: "checks",
ping: "checks",
@@ -11,7 +10,8 @@ const TYPE_MAP = {
class BufferService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, logger }) {
constructor({ db, logger, envSettings }) {
this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
this.db = db;
this.logger = logger;
this.SERVICE_NAME = SERVICE_NAME;
@@ -21,14 +21,14 @@ class BufferService {
hardwareChecks: [],
};
this.OPERATION_MAP = {
checks: this.db.createChecks,
pagespeedChecks: this.db.createPageSpeedChecks,
hardwareChecks: this.db.createHardwareChecks,
checks: this.db.checkModule.createChecks,
pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks,
hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks,
};
this.scheduleNextFlush();
this.logger.info({
message: `Buffer service initialized, flushing every ${BUFFER_TIMEOUT / 1000}s`,
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
service: this.SERVICE_NAME,
method: "constructor",
});
@@ -66,7 +66,7 @@ class BufferService {
// Schedule the next flush only after the current one completes
this.scheduleNextFlush();
}
}, BUFFER_TIMEOUT);
}, this.BUFFER_TIMEOUT);
}
async flushBuffers() {
let items = 0;
@@ -81,7 +81,7 @@ class NotificationService {
}
async notifyAll({ notificationIDs, subject, html, content }) {
const notifications = await this.db.getNotificationsByIds(notificationIDs);
const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs);
// Map each notification to a test promise
const promises = notifications.map(async (notification) => {
@@ -122,7 +122,7 @@ class StatusService {
this.insertCheck(networkResponse);
try {
const { monitorId, status, code } = networkResponse;
const monitor = await this.db.getMonitorById(monitorId);
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
// Update running stats
this.updateRunningStats({ monitor, networkResponse });
+29 -5
View File
@@ -1,17 +1,20 @@
const SERVICE_NAME = "ServiceRegistry";
import logger from "../../utils/logger.js";
class ServiceRegistry {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
constructor({ logger }) {
this.services = {};
this.logger = logger;
}
get serviceName() {
return ServiceRegistry.SERVICE_NAME;
}
// Instance methods
register(name, service) {
logger.info({
this.logger.info({
message: `Registering service ${name}`,
service: SERVICE_NAME,
method: "register",
@@ -21,7 +24,7 @@ class ServiceRegistry {
get(name) {
if (!this.services[name]) {
logger.error({
this.logger.error({
message: `Service ${name} is not registered`,
service: SERVICE_NAME,
method: "get",
@@ -34,6 +37,27 @@ class ServiceRegistry {
listServices() {
return Object.keys(this.services);
}
static get(name) {
if (!ServiceRegistry.instance) {
throw new Error("ServiceRegistry not initialized");
}
return ServiceRegistry.instance.get(name);
}
static register(name, service) {
if (!ServiceRegistry.instance) {
throw new Error("ServiceRegistry not initialized");
}
return ServiceRegistry.instance.register(name, service);
}
static listServices() {
if (!ServiceRegistry.instance) {
throw new Error("ServiceRegistry not initialized");
}
return ServiceRegistry.instance.listServices();
}
}
export default new ServiceRegistry();
export default ServiceRegistry;
+4 -18
View File
@@ -1,22 +1,14 @@
const SERVICE_NAME = "SettingsService";
const envConfig = {
nodeEnv: process.env.NODE_ENV,
logLevel: process.env.LOG_LEVEL,
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
systemEmailPort: process.env.SYSTEM_EMAIL_PORT,
systemEmailUser: process.env.SYSTEM_EMAIL_USER,
systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS,
systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD,
jwtSecret: process.env.JWT_SECRET,
jwtTTL: process.env.TOKEN_TTL,
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
nodeEnv: process.env.NODE_ENV,
logLevel: process.env.LOG_LEVEL,
clientHost: process.env.CLIENT_HOST,
dbConnectionString: process.env.DB_CONNECTION_STRING,
redisUrl: process.env.REDIS_URL,
callbackUrl: process.env.CALLBACK_URL,
port: process.env.PORT,
pagespeedApiKey: process.env.PAGESPEED_API_KEY,
uprockApiKey: process.env.UPROCK_API_KEY,
};
/**
* SettingsService
@@ -46,13 +38,7 @@ class SettingsService {
loadSettings() {
return this.settings;
}
/**
* Reload settings by calling loadSettings.
* @returns {Promise<Object>} The reloaded settings.
*/
reloadSettings() {
return this.loadSettings();
}
/**
* Get the current settings.
* @returns {Object} The current settings.
+1 -1
View File
@@ -1,4 +1,4 @@
import logger from "./utils/logger.js";
import { logger } from "./utils/logger.js";
export const initShutdownListener = (server, services) => {
const SERVICE_NAME = "Server";
+7 -5
View File
@@ -6,7 +6,8 @@ const SERVICE_NAME = "Logger";
class Logger {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
constructor({ envSettings }) {
this.envSettings = envSettings;
this.logCache = [];
this.maxCacheSize = 1000;
const consoleFormat = format.printf(({ level, message, service, method, details, timestamp, stack }) => {
@@ -45,7 +46,7 @@ class Logger {
return msg;
});
const logLevel = process.env.LOG_LEVEL || "info";
const logLevel = this.envSettings.logLevel || "info";
this.logger = createLogger({
level: logLevel,
@@ -145,7 +146,8 @@ class Logger {
}
}
const logger = new Logger();
export { Logger };
export default Logger;
export default logger;
// Legacy logger
const logger = new Logger({ envSettings: { logLevel: "debug" } });
export { logger };
+9
View File
@@ -416,6 +416,15 @@ const updateAppSettingsBodyValidation = joi.object({
systemEmailIgnoreTLS: joi.boolean(),
systemEmailRequireTLS: joi.boolean(),
systemEmailRejectUnauthorized: joi.boolean(),
globalThresholds: joi
.object({
cpu: joi.number().min(1).max(100).allow("").optional(),
memory: joi.number().min(1).max(100).allow("").optional(),
disk: joi.number().min(1).max(100).allow("").optional(),
temperature: joi.number().min(1).max(150).allow("").optional(),
})
.optional(),
});
//****************************************