move to src

This commit is contained in:
Alex Holliday committed 2025-07-25 15:33:48 -07:00
1 parent acc910cbc0
commit 1c47c8ce2c
127 files changed
+71 -19

No files matched your search

+385
View File
@@ -0,0 +1,385 @@
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";
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
recent: new Date(new Date().setDate(new Date().getDate() - 2)),
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: 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;
}
};
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;
}
}
//Sort
sortOrder = sortOrder === "asc" ? 1 : -1;
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
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],
},
checks: {
$ifNull: ["$checks", []],
},
},
},
]);
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],
},
}),
};
// 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;
}
}
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 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");
}
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,
};
@@ -0,0 +1,44 @@
const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
resolvedChecks: {
$sum: {
$cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
downChecks: {
$sum: {
$cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
cannotResolveChecks: {
$sum: {
$cond: [{ $eq: ["$statusCode", 5000] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
},
},
];
};
export { buildChecksSummaryByTeamIdPipeline };
+48
View File
@@ -0,0 +1,48 @@
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 };
+70
View File
@@ -0,0 +1,70 @@
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;
}
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 };
+89
View File
@@ -0,0 +1,89 @@
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);
}
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);
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteTokenAndDelete";
throw error;
}
};
export { requestInviteToken, getInviteToken, getInviteTokenAndDelete };
+192
View File
@@ -0,0 +1,192 @@
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,
});
// 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 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;
}
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort);
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
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,
};
+766
View File
@@ -0,0 +1,766 @@
import Monitor from "../../models/Monitor.js";
import MonitorStats from "../../models/MonitorStats.js";
import Check from "../../models/Check.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { ObjectId } from "mongodb";
import {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
} from "./monitorModuleQueries.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json");
const demoMonitors = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8"));
const SERVICE_NAME = "monitorModule";
const CHECK_MODEL_LOOKUP = {
http: Check,
ping: Check,
docker: Check,
port: Check,
pagespeed: PageSpeedCheck,
hardware: HardwareCheck,
};
/**
* Get all monitors
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Array<Monitor>>}
* @throws {Error}
*/
const getAllMonitors = async (req, res) => {
try {
const monitors = await Monitor.find();
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllMonitors";
throw error;
}
};
/**
* Function to calculate uptime duration based on the most recent check.
* @param {Array} checks Array of check objects.
* @returns {number} Uptime duration in ms.
*/
const calculateUptimeDuration = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
const latestCheck = new Date(checks[0].createdAt);
let latestDownCheck = 0;
for (let i = checks.length - 1; i >= 0; i--) {
if (checks[i].status === false) {
latestDownCheck = new Date(checks[i].createdAt);
break;
}
}
// If no down check is found, uptime is from the last check to now
if (latestDownCheck === 0) {
return Date.now() - new Date(checks[checks.length - 1].createdAt);
}
// Otherwise the uptime is from the last check to the last down check
return latestCheck - latestDownCheck;
};
/**
* Helper function to get duration since last check
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
const getLastChecked = (checks) => {
if (!checks || checks.length === 0) {
return 0; // Handle case when no checks are available
}
// Data is sorted newest->oldest, so last check is the most recent
return new Date() - new Date(checks[0].createdAt);
};
/**
* Helper function to get latestResponseTime
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
const getLatestResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
return checks[0]?.responseTime ?? 0;
};
/**
* Helper function to get average response time
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
const getAverageResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
const validChecks = checks.filter((check) => typeof check.responseTime === "number");
if (validChecks.length === 0) {
return 0;
}
const aggResponseTime = validChecks.reduce((sum, check) => {
return sum + check.responseTime;
}, 0);
return aggResponseTime / validChecks.length;
};
/**
* Helper function to get percentage 24h uptime
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
const getUptimePercentage = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
const upCount = checks.reduce((count, check) => {
return check.status === true ? count + 1 : count;
}, 0);
return (upCount / checks.length) * 100;
};
/**
* Helper function to get all incidents
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
const getIncidents = (checks) => {
if (!checks || checks.length === 0) {
return 0; // Handle case when no checks are available
}
return checks.reduce((acc, check) => {
return check.status === false ? (acc += 1) : acc;
}, 0);
};
/**
* Get date range parameters
* @param {string} dateRange - 'day' | 'week' | 'month' | 'all'
* @returns {Object} Start and end dates
*/
const getDateRange = (dateRange) => {
const startDates = {
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: new Date(0),
};
return {
start: startDates[dateRange],
end: new Date(),
};
};
/**
* Get checks for a monitor
* @param {string} monitorId - Monitor ID
* @param {Object} model - Check model to use
* @param {Object} dateRange - Date range parameters
* @param {number} sortOrder - Sort order (1 for ascending, -1 for descending)
* @returns {Promise<Object>} All checks and date-ranged checks
*/
const getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => {
const indexSpec = {
monitorId: 1,
createdAt: sortOrder, // This will be 1 or -1
};
const [checksAll, checksForDateRange] = await Promise.all([
model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
model
.find({
monitorId,
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
})
.hint(indexSpec)
.lean(),
]);
return { checksAll, checksForDateRange };
};
/**
* Process checks for display
* @param {Array} checks - Checks to process
* @param {number} numToDisplay - Number of checks to display
* @param {boolean} normalize - Whether to normalize the data
* @returns {Array} Processed checks
*/
const processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => {
let processedChecks = checks;
if (numToDisplay && checks.length > numToDisplay) {
const n = Math.ceil(checks.length / numToDisplay);
processedChecks = checks.filter((_, index) => index % n === 0);
}
return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks;
};
/**
* Get time-grouped checks based on date range
* @param {Array} checks Array of check objects
* @param {string} dateRange 'day' | 'week' | 'month'
* @returns {Object} Grouped checks by time period
*/
const groupChecksByTime = (checks, dateRange) => {
return checks.reduce((acc, check) => {
// Validate the date
const checkDate = new Date(check.createdAt);
if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) {
return acc;
}
const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0];
if (!acc[time]) {
acc[time] = { time, checks: [] };
}
acc[time].checks.push(check);
return acc;
}, {});
};
/**
* Calculate aggregate stats for a group of checks
* @param {Object} group Group of checks
* @returns {Object} Stats for the group
*/
const calculateGroupStats = (group) => {
const totalChecks = group.checks.length;
const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime));
return {
time: group.time,
uptimePercentage: getUptimePercentage(group.checks),
totalChecks,
totalIncidents: group.checks.filter((check) => !check.status).length,
avgResponseTime:
checksWithResponseTime.length > 0
? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length
: 0,
};
};
/**
* Get uptime details by monitor ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => {
try {
const dates = getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT%H:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const results = await Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
const monitorData = results[0];
monitorData.groupedUpChecks = NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100);
monitorData.groupedDownChecks = NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100);
const normalizedGroupChecks = NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100);
monitorData.groupedChecks = normalizedGroupChecks;
const monitorStats = await MonitorStats.findOne({ monitorId });
return { monitorData, monitorStats };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUptimeDetailsById";
throw error;
}
};
/**
* Get stats by monitor ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const getMonitorStatsById = async ({ monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
// Get monitor, if we can't find it, abort with error
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(stringService.getDbFindMonitorById(monitorId));
}
// Get query params
const sort = sortOrder === "asc" ? 1 : -1;
// Get Checks for monitor in date range requested
const model = CHECK_MODEL_LOOKUP[monitor.type];
const dates = getDateRange(dateRange);
const { checksAll, checksForDateRange } = await getMonitorChecks(monitorId, model, dates, sort);
// Build monitor stats
const monitorStats = {
...monitor.toObject(),
uptimeDuration: calculateUptimeDuration(checksAll),
lastChecked: getLastChecked(checksAll),
latestResponseTime: getLatestResponseTime(checksAll),
periodIncidents: getIncidents(checksForDateRange),
periodTotalChecks: checksForDateRange.length,
checks: processChecksForDisplay(NormalizeData, checksForDateRange, numToDisplay, normalize),
};
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port") {
// HTTP/PING Specific stats
monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange);
monitorStats.periodUptime = getUptimePercentage(checksForDateRange);
const groupedChecks = groupChecksByTime(checksForDateRange, dateRange);
monitorStats.aggregateData = Object.values(groupedChecks).map(calculateGroupStats);
}
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorStatsById";
throw error;
}
};
const getHardwareDetailsById = async ({ monitorId, dateRange }) => {
try {
const monitor = await Monitor.findById(monitorId);
const dates = getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT%H:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const hardwareStats = await HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const monitorStats = {
...monitor.toObject(),
stats: hardwareStats[0],
};
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getHardwareDetailsById";
throw error;
}
};
/**
* Get a monitor by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const getMonitorById = async (monitorId) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
const error = new Error(stringService.getDbFindMonitorById(monitorId));
error.status = 404;
throw error;
}
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorById";
throw error;
}
};
const getMonitorsByIds = async (monitorIds) => {
try {
const objectIds = monitorIds.map((id) => new ObjectId(id));
return await Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean();
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsByIds";
throw error;
}
};
const getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const summaryResult = await Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage }));
const summary = summaryResult[0];
const monitors = await Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order }));
const filteredMonitors = await Monitor.aggregate(
buildFilteredMonitorsByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
);
const normalizedFilteredMonitors = filteredMonitors.map((monitor) => {
if (!monitor.checks) {
return monitor;
}
monitor.checks = NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
};
const getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
try {
const matchStage = { teamId: new ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (explain === true) {
return Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats");
}
const queryResult = await Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }));
const { monitors, summary } = queryResult?.[0] ?? {};
return { monitors, summary };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsAndSummaryByTeamId";
throw error;
}
};
const getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => {
try {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (explain === true) {
return Monitor.aggregate(
buildMonitorsWithChecksByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
).explain("executionStats");
}
const queryResult = await Monitor.aggregate(
buildMonitorsWithChecksByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
);
const monitors = queryResult[0]?.monitors;
const count = queryResult[0]?.count;
const normalizedFilteredMonitors = monitors.map((monitor) => {
if (!monitor.checks) {
return monitor;
}
monitor.checks = NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { count, monitors: normalizedFilteredMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsWithChecksByTeamId";
throw error;
}
};
/**
* Create a monitor
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const createMonitor = async ({ body, teamId, userId }) => {
try {
const monitor = new Monitor({ ...body, teamId, userId });
const saved = await monitor.save();
return saved;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMonitor";
throw error;
}
};
/**
* Create bulk monitors
* @async
* @param {Express.Request} req
* @returns {Promise<Monitors>}
* @throws {Error}
*/
const createBulkMonitors = async (req) => {
try {
const monitors = req.map((item) => new Monitor({ ...item, notifications: undefined }));
await Monitor.bulkSave(monitors);
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createBulkMonitors";
throw error;
}
};
/**
* Delete a monitor by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const deleteMonitor = async ({ teamId, monitorId }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const deletedMonitor = await Monitor.findOneAndDelete({ _id: monitorId, teamId });
if (!deletedMonitor) {
throw new Error(stringService.getDbFindMonitorById(monitorId));
}
return deletedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitor";
throw error;
}
};
/**
* DELETE ALL MONITORS (TEMP)
*/
const deleteAllMonitors = async (teamId) => {
try {
const monitors = await Monitor.find({ teamId });
const { deletedCount } = await Monitor.deleteMany({ teamId });
return { monitors, deletedCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteAllMonitors";
throw error;
}
};
/**
* Delete all monitors associated with a user ID
* @async
* @param {string} userId - The ID of the user whose monitors are to be deleted.
* @returns {Promise} A promise that resolves when the operation is complete.
*/
const deleteMonitorsByUserId = async (userId) => {
try {
const result = await Monitor.deleteMany({ userId: userId });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitorsByUserId";
throw error;
}
};
/**
* Edit a monitor by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const editMonitor = async ({ monitorId, body }) => {
try {
const editedMonitor = await Monitor.findByIdAndUpdate(monitorId, body, {
new: true,
});
return editedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMonitor";
throw error;
}
};
const addDemoMonitors = async (userId, teamId) => {
try {
const demoMonitorsToInsert = demoMonitors.map((monitor) => {
return {
userId,
teamId,
name: monitor.name,
description: monitor.name,
type: "http",
url: monitor.url,
interval: 60000,
};
});
const insertedMonitors = await Monitor.insertMany(demoMonitorsToInsert);
return insertedMonitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "addDemoMonitors";
throw error;
}
};
const pauseMonitor = async ({ monitorId }) => {
try {
const monitor = await Monitor.findOneAndUpdate(
{ _id: monitorId },
[
{
$set: {
isActive: { $not: "$isActive" },
status: "$$REMOVE",
},
},
],
{ new: true }
);
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "pauseMonitor";
throw error;
}
};
export {
getAllMonitors,
getMonitorStatsById,
getMonitorById,
getMonitorsByIds,
getMonitorsByTeamId,
getMonitorsAndSummaryByTeamId,
getMonitorsWithChecksByTeamId,
getUptimeDetailsById,
createMonitor,
createBulkMonitors,
deleteMonitor,
deleteAllMonitors,
deleteMonitorsByUserId,
editMonitor,
addDemoMonitors,
getHardwareDetailsById,
pauseMonitor,
};
// Helper functions
export {
calculateUptimeDuration,
getLastChecked,
getLatestResponseTime,
getAverageResponseTime,
getUptimePercentage,
getIncidents,
getDateRange,
getMonitorChecks,
processChecksForDisplay,
groupChecksByTime,
calculateGroupStats,
};
// limit 25
// page 1
// rowsPerPage 25
// filter undefined
// field name
// order asc
// skip 25
// sort { name: 1 }
// filteredMonitors []
// limit 25
// page NaN
// rowsPerPage 25
// filter undefined
// field name
// order asc
// skip 0
// sort { name: 1 }
+869
View File
@@ -0,0 +1,869 @@
import { ObjectId } from "mongodb";
const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
return [
{
$match: {
monitorId: new ObjectId(monitorId),
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$sort: {
createdAt: 1,
},
},
{
$facet: {
// For the response time chart, should return checks for date window
// Grouped by: {day: hour}, {week: day}, {month: day}
uptimePercentage: [
{
$group: {
_id: null,
upChecks: {
$sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] },
},
totalChecks: { $sum: 1 },
},
},
{
$project: {
_id: 0,
percentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
},
],
groupedAvgResponseTime: [
{
$group: {
_id: null,
avgResponseTime: {
$avg: "$responseTime",
},
},
},
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
avgResponseTime: {
$avg: "$responseTime",
},
totalChecks: {
$sum: 1,
},
},
},
{
$sort: {
_id: 1,
},
},
],
// Up checks grouped by: {day: hour}, {week: day}, {month: day}
groupedUpChecks: [
{
$match: {
status: true,
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
totalChecks: {
$sum: 1,
},
avgResponseTime: {
$avg: "$responseTime",
},
},
},
{
$sort: { _id: 1 },
},
],
// Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window
groupedDownChecks: [
{
$match: {
status: false,
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
totalChecks: {
$sum: 1,
},
avgResponseTime: {
$avg: "$responseTime",
},
},
},
{
$sort: { _id: 1 },
},
],
},
},
{
$lookup: {
from: "monitors",
let: { monitor_id: { $toObjectId: monitorId } },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$monitor_id"] },
},
},
{
$project: {
_id: 1,
teamId: 1,
name: 1,
status: 1,
interval: 1,
type: 1,
url: 1,
isActive: 1,
notifications: 1,
},
},
],
as: "monitor",
},
},
{
$project: {
groupedAvgResponseTime: {
$arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0],
},
groupedChecks: "$groupedChecks",
groupedUpChecks: "$groupedUpChecks",
groupedDownChecks: "$groupedDownChecks",
groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] },
monitor: { $arrayElemAt: ["$monitor", 0] },
},
},
];
};
const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
return [
{
$match: {
monitorId: monitor._id,
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$sort: {
createdAt: 1,
},
},
{
$facet: {
aggregateData: [
{
$group: {
_id: null,
latestCheck: {
$last: "$$ROOT",
},
totalChecks: {
$sum: 1,
},
},
},
],
upChecks: [
{
$match: {
status: true,
},
},
{
$group: {
_id: null,
totalChecks: {
$sum: 1,
},
},
},
],
checks: [
{
$limit: 1,
},
{
$project: {
diskCount: {
$size: "$disk",
},
},
},
{
$lookup: {
from: "hardwarechecks",
let: {
diskCount: "$diskCount",
},
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$monitorId", monitor._id] }, { $gte: ["$createdAt", dates.start] }, { $lte: ["$createdAt", dates.end] }],
},
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
avgCpuUsage: {
$avg: "$cpu.usage_percent",
},
avgMemoryUsage: {
$avg: "$memory.usage_percent",
},
avgTemperatures: {
$push: {
$ifNull: ["$cpu.temperature", [0]],
},
},
disks: {
$push: "$disk",
},
},
},
{
$project: {
_id: 1,
avgCpuUsage: 1,
avgMemoryUsage: 1,
avgTemperature: {
$map: {
input: {
$range: [
0,
{
$size: {
// Handle null temperatures array
$ifNull: [
{ $arrayElemAt: ["$avgTemperatures", 0] },
[0], // Default to single-element array if null
],
},
},
],
},
as: "index",
in: {
$avg: {
$map: {
input: "$avgTemperatures",
as: "tempArray",
in: {
$ifNull: [
{ $arrayElemAt: ["$$tempArray", "$$index"] },
0, // Default to 0 if element is null
],
},
},
},
},
},
},
disks: {
$map: {
input: {
$range: [0, "$$diskCount"],
},
as: "diskIndex",
in: {
name: {
$concat: [
"disk",
{
$toString: "$$diskIndex",
},
],
},
readSpeed: {
$avg: {
$map: {
input: "$disks",
as: "diskArray",
in: {
$arrayElemAt: ["$$diskArray.read_speed_bytes", "$$diskIndex"],
},
},
},
},
writeSpeed: {
$avg: {
$map: {
input: "$disks",
as: "diskArray",
in: {
$arrayElemAt: ["$$diskArray.write_speed_bytes", "$$diskIndex"],
},
},
},
},
totalBytes: {
$avg: {
$map: {
input: "$disks",
as: "diskArray",
in: {
$arrayElemAt: ["$$diskArray.total_bytes", "$$diskIndex"],
},
},
},
},
freeBytes: {
$avg: {
$map: {
input: "$disks",
as: "diskArray",
in: {
$arrayElemAt: ["$$diskArray.free_bytes", "$$diskIndex"],
},
},
},
},
usagePercent: {
$avg: {
$map: {
input: "$disks",
as: "diskArray",
in: {
$arrayElemAt: ["$$diskArray.usage_percent", "$$diskIndex"],
},
},
},
},
},
},
},
},
},
],
as: "hourlyStats",
},
},
{
$unwind: "$hourlyStats",
},
{
$replaceRoot: {
newRoot: "$hourlyStats",
},
},
],
},
},
{ $unwind: "$checks" },
{ $sort: { "checks._id": 1 } },
{
$group: {
_id: "$_id",
checks: { $push: "$checks" },
aggregateData: { $first: "$aggregateData" },
upChecks: { $first: "$upChecks" },
},
},
{
$project: {
aggregateData: {
$arrayElemAt: ["$aggregateData", 0],
},
upChecks: {
$arrayElemAt: ["$upChecks", 0],
},
checks: 1,
},
},
];
};
const buildMonitorStatsPipeline = (monitor) => {
return [
{
$match: {
monitorId: monitor._id,
},
},
{
$project: {
avgResponseTime: 1,
uptimePercentage: 1,
totalChecks: 1,
timeSinceLastCheck: {
$subtract: [Date.now(), "$lastCheckTimestamp"],
},
lastCheckTimestamp: 1,
uptBurnt: { $toString: "$uptBurnt" },
},
},
];
};
const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
];
};
const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => {
const sort = { [field]: order === "asc" ? 1 : -1 };
return [
{ $match: matchStage },
{ $sort: sort },
{
$project: {
_id: 1,
name: 1,
type: 1,
port: 1,
},
},
];
};
const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: { name: 1 } },
{
$project: {
_id: 1,
name: 1,
type: 1,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
monitors: 1,
},
},
];
};
const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
// Match name
if (typeof filter !== "undefined" && field === "name") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
// Match isActive
if (typeof filter !== "undefined" && field === "isActive") {
matchStage.isActive = filter === "true" ? true : false;
}
if (typeof filter !== "undefined" && field === "status") {
matchStage.status = filter === "true" ? true : false;
}
// Match type
if (typeof filter !== "undefined" && field === "type") {
matchStage.type = filter;
}
const monitorsPipeline = [
{ $sort: sort },
{ $skip: skip },
...limitStage,
{
$project: {
_id: 1,
name: 1,
description: 1,
type: 1,
url: 1,
isActive: 1,
createdAt: 1,
updatedAt: 1,
uptimePercentage: 1,
status: 1,
},
},
];
// Add checks
if (limit) {
let checksCollection = "checks";
if (type === "pagespeed") {
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
}
monitorsPipeline.push({
$lookup: {
from: checksCollection,
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { updatedAt: -1 } },
{ $limit: limit },
{
$project: {
_id: 1,
status: 1,
responseTime: 1,
statusCode: 1,
createdAt: 1,
updatedAt: 1,
originalResponseTime: 1,
},
},
],
as: "checks",
},
});
}
const pipeline = [
{ $match: matchStage },
{
$facet: {
count: [{ $count: "monitorsCount" }],
monitors: monitorsPipeline,
},
},
{
$project: {
count: { $arrayElemAt: ["$count", 0] },
monitors: 1,
},
},
];
return pipeline;
};
const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
if (typeof filter !== "undefined" && field === "name") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
if (typeof filter !== "undefined" && field === "status") {
matchStage.status = filter === "true";
}
const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage];
// Add checks
if (limit) {
let checksCollection = "checks";
if (type === "pagespeed") {
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
}
pipeline.push({
$lookup: {
from: checksCollection,
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: limit },
],
as: "checks",
},
});
}
return pipeline;
};
const buildGetMonitorsByTeamIdPipeline = (req) => {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build the match stage
const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: sort },
{
$project: {
_id: 1,
name: 1,
},
},
],
filteredMonitors: [
...(filter !== undefined
? [
{
$match: {
$or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }],
},
},
]
: []),
{ $sort: sort },
{ $skip: skip },
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
...(limit
? [
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "standardchecks",
},
},
]
: []),
...(limit
? [
{
$lookup: {
from: "pagespeedchecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "pagespeedchecks",
},
},
]
: []),
...(limit
? [
{
$lookup: {
from: "hardwarechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "hardwarechecks",
},
},
]
: []),
{
$addFields: {
checks: {
$switch: {
branches: [
{
case: { $in: ["$type", ["http", "ping", "docker", "port"]] },
then: "$standardchecks",
},
{
case: { $eq: ["$type", "pagespeed"] },
then: "$pagespeedchecks",
},
{
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
],
default: [],
},
},
},
},
{
$project: {
standardchecks: 0,
pagespeedchecks: 0,
hardwarechecks: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
filteredMonitors: 1,
monitors: 1,
},
},
];
};
export {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildMonitorStatsPipeline,
buildGetMonitorsByTeamIdPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
};
@@ -0,0 +1,44 @@
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;
}
};
/**
* 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 };
+125
View File
@@ -0,0 +1,125 @@
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;
}
};
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");
}
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 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,
};
+57
View File
@@ -0,0 +1,57 @@
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;
}
};
export { createPageSpeedCheck, createPageSpeedChecks, deletePageSpeedChecksByMonitorId };
+86
View File
@@ -0,0 +1,86 @@
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;
}
};
const validateRecoveryToken = async (candidateToken) => {
const stringService = serviceRegistry.get(StringService.SERVICE_NAME);
try {
const recoveryToken = await RecoveryToken.findOne({
token: candidateToken,
});
if (recoveryToken !== null) {
return recoveryToken;
} else {
throw new Error(stringService.dbTokenNotFound);
}
} 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);
}
const match = await user.comparePassword(newPassword);
if (match === true) {
throw new Error(stringService.dbResetPasswordBadMatch);
}
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 };
+41
View File
@@ -0,0 +1,41 @@
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;
}
};
const updateAppSettings = async (newSettings) => {
try {
const update = { $set: { ...newSettings } };
if (newSettings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
}
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 };
+241
View File
@@ -0,0 +1,241 @@
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;
}
};
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;
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: [],
};
}
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: "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"],
},
},
},
{ $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: 1,
},
},
]);
if (!statusPageQuery.length) {
const error = new Error(stringService.statusPageNotFound);
error.status = 404;
throw error;
}
const { statusPage, monitors } = statusPageQuery[0];
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,
};
+261
View File
@@ -0,0 +1,261 @@
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 checkSuperadmin = async () => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
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,
};
// 2. Get the avatar sized image
const avatar = await generateAvatarImage(imageFile);
userData.avatarImage = avatar;
}
// 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();
}
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;
}
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);
}
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}
*/
const updateUser = async ({ userId, user, file }) => {
if (!userId) {
throw new Error("No user in request");
}
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;
}
// ******************************************
// 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);
}
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,
};