Merge pull request #3174 from bluewave-labs/feat/status-page-service-and-repo
feat/status page service and repo
This commit is contained in:
21 files changed
+356
-357
No files matched your search
@@ -148,7 +148,7 @@ const StatusPageBarChart = ({ checks = [] }) => {
|
||||
</>
|
||||
}
|
||||
placement="top"
|
||||
key={`check-${check?._id}`}
|
||||
key={`check-${check?.id}`}
|
||||
slotProps={{
|
||||
popper: {
|
||||
className: "bar-tooltip",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckCircle2,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
@@ -82,6 +83,7 @@ const iconComponents = {
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckCircle2,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useState } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
|
||||
const useCreateStatusPage = (isCreate, url) => {
|
||||
const useCreateStatusPage = (isCreate) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const createStatusPage = async ({ form }) => {
|
||||
const createStatusPage = async ({ form, id }) => {
|
||||
console.log(id);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await networkService.createStatusPage({ form, isCreate, url });
|
||||
await networkService.createStatusPage({ form, isCreate, id });
|
||||
return true;
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
|
||||
@@ -54,10 +54,10 @@ const CreateStatusPage = () => {
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, isLoading, networkError] = useMonitorsFetch();
|
||||
const [createStatusPage] = useCreateStatusPage(isCreate);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [createStatusPage] = useCreateStatusPage(isCreate);
|
||||
const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] =
|
||||
useStatusPageFetch(isCreate, url);
|
||||
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
|
||||
@@ -144,13 +144,12 @@ const CreateStatusPage = () => {
|
||||
logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null },
|
||||
};
|
||||
|
||||
console.log(toSubmit);
|
||||
const { error } = statusPageValidation.validate(toSubmit, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (typeof error === "undefined") {
|
||||
const success = await createStatusPage({ form });
|
||||
const success = await createStatusPage({ form, id: statusPage?.id });
|
||||
if (success) {
|
||||
createToast({
|
||||
body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"),
|
||||
|
||||
@@ -14,7 +14,7 @@ const useStatusPagesFetch = () => {
|
||||
const fetchStatusPages = async () => {
|
||||
try {
|
||||
const res = await networkService.getStatusPagesByTeamId();
|
||||
setStatusPages(res?.data?.data);
|
||||
setStatusPages(res?.data?.data || []);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
|
||||
@@ -909,7 +909,7 @@ class NetworkService {
|
||||
}
|
||||
|
||||
async createStatusPage(config) {
|
||||
const { form, isCreate } = config;
|
||||
const { form, isCreate, id } = config;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("type", form.type);
|
||||
@@ -955,7 +955,7 @@ class NetworkService {
|
||||
return this.axiosInstance.post(`/status-page`, fd, {});
|
||||
}
|
||||
|
||||
return this.axiosInstance.put(`/status-page`, fd, {});
|
||||
return this.axiosInstance.put(`/status-page/${id}`, fd, {});
|
||||
}
|
||||
|
||||
async deleteStatusPage(config) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const initializeControllers = (services: InitializedServices): Initialize
|
||||
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
||||
queueController: new QueueController(services.jobQueue),
|
||||
logController: new LogController(services.logger),
|
||||
statusPageController: new StatusPageController(services.db),
|
||||
statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService),
|
||||
notificationController: new NotificationController(services.notificationsService, services.monitorsRepository),
|
||||
diagnosticController: new DiagnosticController(services.diagnosticService),
|
||||
incidentController: new IncidentController(services.incidentService),
|
||||
|
||||
@@ -22,6 +22,7 @@ import DiagnosticService from "../service/business/diagnosticService.js";
|
||||
import InviteService from "../service/business/inviteService.js";
|
||||
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
||||
import { MonitorService } from "@/service/index.js";
|
||||
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
|
||||
import IncidentService from "../service/business/incidentService.js";
|
||||
import axios from "axios";
|
||||
import got from "got";
|
||||
@@ -42,7 +43,6 @@ import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
|
||||
// DB Modules
|
||||
import { NormalizeData } from "../utils/dataUtils.js";
|
||||
import { GenerateAvatarImage } from "../utils/imageProcessing.js";
|
||||
import { ParseBoolean } from "../utils/utils.js";
|
||||
|
||||
@@ -50,16 +50,13 @@ import { ParseBoolean } from "../utils/utils.js";
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import User from "../db/models/User.js";
|
||||
import InviteToken from "../db/models/Invite.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 NotificationModel from "../db/models/Notification.js";
|
||||
import RecoveryToken from "../db/models/RecoveryToken.js";
|
||||
import AppSettings from "../db/models/AppSettings.js";
|
||||
import Incident from "../db/models/Incident.js";
|
||||
|
||||
import StatusPageModule from "../db/modules/statusPageModule.js";
|
||||
import IncidentModule from "../db/modules/incidentModule.js";
|
||||
|
||||
// repositories
|
||||
@@ -109,6 +106,7 @@ export type InitializedServices = {
|
||||
errorService: any;
|
||||
logger: any;
|
||||
notificationsService: INotificationsService;
|
||||
statusPageService: IStatusPageService;
|
||||
|
||||
// Repositories
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
@@ -137,13 +135,11 @@ export const initializeServices = async ({
|
||||
settingsRepository: ISettingsRepository;
|
||||
}): Promise<InitializedServices> => {
|
||||
// Create DB
|
||||
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings });
|
||||
const incidentModule = new IncidentModule({ logger, Incident, Monitor, User });
|
||||
|
||||
const db = new MongoDB({
|
||||
logger,
|
||||
envSettings,
|
||||
statusPageModule,
|
||||
incidentModule,
|
||||
});
|
||||
|
||||
@@ -273,6 +269,8 @@ export const initializeServices = async ({
|
||||
statusPagesRepository,
|
||||
});
|
||||
|
||||
const statusPageService = new StatusPageService(statusPagesRepository);
|
||||
|
||||
const services = {
|
||||
//v1
|
||||
settingsService,
|
||||
@@ -292,6 +290,7 @@ export const initializeServices = async ({
|
||||
errorService,
|
||||
logger,
|
||||
notificationsService,
|
||||
statusPageService,
|
||||
|
||||
// Repositories
|
||||
monitorsRepository,
|
||||
|
||||
@@ -99,6 +99,13 @@ const requireTeamId = (teamId?: string): string => {
|
||||
return teamId;
|
||||
};
|
||||
|
||||
const requireUserId = (userId?: string): string => {
|
||||
if (!userId) {
|
||||
throw new AppError({ message: "User ID is required", status: 400 });
|
||||
}
|
||||
return userId;
|
||||
};
|
||||
|
||||
export {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
@@ -108,4 +115,5 @@ export {
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
requireUserId,
|
||||
};
|
||||
@@ -2,14 +2,25 @@ import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||
import { IStatusPageService } from "@/service/business/statusPageService.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { MonitorWithChecks } from "@/types/index.js";
|
||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
class StatusPageController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private db: any;
|
||||
constructor(db: any) {
|
||||
this.db = db;
|
||||
private statusPageService: IStatusPageService;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private settingsService: ISettingsService;
|
||||
constructor(statusPageService: IStatusPageService, monitorsRepository: IMonitorsRepository, settingsService: ISettingsService) {
|
||||
this.statusPageService = statusPageService;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -21,13 +32,10 @@ class StatusPageController {
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
await imageValidation.validateAsync(req.file);
|
||||
|
||||
const { id, teamId } = req.user ?? {};
|
||||
const statusPage = await this.db.statusPageModule.createStatusPage({
|
||||
statusPageData: req.body,
|
||||
image: req.file,
|
||||
userId: id,
|
||||
teamId,
|
||||
});
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userId = requireUserId(req?.user?.id);
|
||||
const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page created successfully",
|
||||
@@ -42,8 +50,12 @@ class StatusPageController {
|
||||
try {
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
await imageValidation.validateAsync(req.file);
|
||||
|
||||
const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const statusPageId = req.params.id;
|
||||
if (!statusPageId) {
|
||||
throw new AppError({ message: "Status page ID is required", status: 400 });
|
||||
}
|
||||
const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, req.body);
|
||||
if (statusPage === null) {
|
||||
throw new AppError({ message: "Status page not found", status: 404 });
|
||||
}
|
||||
@@ -57,29 +69,32 @@ class StatusPageController {
|
||||
}
|
||||
};
|
||||
|
||||
getStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const statusPage = await this.db.statusPageModule.getStatusPage();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page retrieved successfully",
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getStatusPageParamValidation.validateAsync(req.params);
|
||||
await getStatusPageQueryValidation.validateAsync(req.query);
|
||||
|
||||
const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url);
|
||||
if (!req.params.url) {
|
||||
throw new AppError({ message: "Status page URL is required", status: 400 });
|
||||
}
|
||||
|
||||
const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url);
|
||||
const settings = await this.settingsService.getDBSettings();
|
||||
const showURL = settings.showURL;
|
||||
|
||||
const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors);
|
||||
const normalizedMonitors = monitors.map((monitor) => {
|
||||
const normalizedChecks = NormalizeData(monitor.checks, 10, 100);
|
||||
if (!showURL) {
|
||||
const { url, port, secret, notifications, ...rest } = monitor;
|
||||
return { ...rest, checks: normalizedChecks };
|
||||
}
|
||||
return { ...monitor, checks: normalizedChecks };
|
||||
});
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page retrieved successfully",
|
||||
data: statusPage,
|
||||
data: { statusPage, monitors: normalizedMonitors },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -87,8 +102,8 @@ class StatusPageController {
|
||||
};
|
||||
getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req.user?.teamId;
|
||||
const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId);
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const statusPages = await this.statusPageService.getStatusPagesByTeamId(teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -99,9 +114,15 @@ class StatusPageController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
deleteStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await this.db.statusPageModule.deleteStatusPage(req.params.url);
|
||||
const statusPageId = req.params.id;
|
||||
if (!statusPageId) {
|
||||
throw new AppError({ message: "Status page ID is required", status: 400 });
|
||||
}
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
await this.statusPageService.deleteStatusPage(statusPageId, teamId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page deleted successfully",
|
||||
|
||||
@@ -4,10 +4,9 @@ import { runMigrations } from "./migration/index.js";
|
||||
class MongoDB {
|
||||
static SERVICE_NAME = "MongoDB";
|
||||
|
||||
constructor({ logger, envSettings, statusPageModule, incidentModule }) {
|
||||
constructor({ logger, envSettings, incidentModule }) {
|
||||
this.logger = logger;
|
||||
this.envSettings = envSettings;
|
||||
this.statusPageModule = statusPageModule;
|
||||
this.incidentModule = incidentModule;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
const SERVICE_NAME = "statusPageModule";
|
||||
|
||||
class StatusPageModule {
|
||||
constructor({ StatusPage, NormalizeData, AppSettings }) {
|
||||
this.StatusPage = StatusPage;
|
||||
this.NormalizeData = NormalizeData;
|
||||
this.AppSettings = AppSettings;
|
||||
}
|
||||
|
||||
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 = "Status page URL must be unique";
|
||||
}
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createStatusPage";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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("Status page not found");
|
||||
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 this.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: ["$metadata.monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $limit: 25 },
|
||||
],
|
||||
as: "monitors.checks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitorstats",
|
||||
localField: "monitors._id",
|
||||
foreignField: "monitorId",
|
||||
as: "monitors.stats",
|
||||
},
|
||||
},
|
||||
{
|
||||
$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"] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"monitors.uptimePercentage": {
|
||||
$arrayElemAt: ["$monitors.stats.uptimePercentage", 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $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("Status page not found");
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { statusPage, monitors } = statusPageQuery[0];
|
||||
|
||||
const appSettings = await this.AppSettings.findOne({ singleton: true }).lean();
|
||||
const showURL = appSettings?.showURL === true;
|
||||
|
||||
const normalizedMonitors = monitors.map((monitor) => {
|
||||
const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100);
|
||||
if (!showURL) {
|
||||
const { url, port, secret, notifications, ...rest } = monitor;
|
||||
return { ...rest, checks: normalizedChecks };
|
||||
}
|
||||
return { ...monitor, checks: normalizedChecks };
|
||||
});
|
||||
|
||||
return { statusPage, monitors: normalizedMonitors };
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getStatusPageByUrl";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
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] } });
|
||||
await this.StatusPage.deleteMany({ subMonitors: { $in: [monitorId] } });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteStatusPageByMonitorId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default StatusPageModule;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js";
|
||||
import { type MonitorType, type Monitor, type MonitorsSummary, type MonitorWithChecks } from "@/types/index.js";
|
||||
|
||||
export interface TeamQueryConfig {
|
||||
limit?: number;
|
||||
@@ -25,6 +25,7 @@ export interface IMonitorsRepository {
|
||||
findAll(): Promise<Monitor[] | null>;
|
||||
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
|
||||
findByIds(monitorIds: string[]): Promise<Monitor[]>;
|
||||
findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise<MonitorWithChecks[]>;
|
||||
|
||||
// update
|
||||
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MonitorModel } from "@/db/models/index.js";
|
||||
import type { MonitorDocument } from "@/db/models/index.js";
|
||||
import type { Monitor, MonitorsSummary } from "@/types/index.js";
|
||||
import mongoose, { type FilterQuery } from "mongoose";
|
||||
import type { Monitor, MonitorsSummary, MonitorWithChecks, Check } from "@/types/index.js";
|
||||
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
|
||||
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
@@ -79,6 +79,70 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
return this.mapDocuments(monitors);
|
||||
};
|
||||
|
||||
findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise<MonitorWithChecks[]> => {
|
||||
if (!monitorIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
|
||||
const pipeline: PipelineStage[] = [
|
||||
{ $match: { _id: { $in: objectIds } } },
|
||||
{
|
||||
$lookup: {
|
||||
from: "checks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [{ $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, { $sort: { createdAt: -1 } }, { $limit: checksCount }],
|
||||
as: "checks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "maintenancewindows",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }],
|
||||
as: "maintenanceWindows",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitorstats",
|
||||
localField: "_id",
|
||||
foreignField: "monitorId",
|
||||
as: "stats",
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
isMaintenance: {
|
||||
$reduce: {
|
||||
input: "$maintenanceWindows",
|
||||
initialValue: false,
|
||||
in: {
|
||||
$or: [
|
||||
"$$value",
|
||||
{
|
||||
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
maintenanceWindows: 0,
|
||||
stats: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const documents = await MonitorModel.aggregate(pipeline);
|
||||
return documents.map((doc) => this.toEntityWithChecks(doc));
|
||||
};
|
||||
|
||||
findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise<number> => {
|
||||
const { type } = config ?? {};
|
||||
|
||||
@@ -249,6 +313,90 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
private toEntityWithChecks = (doc: any): MonitorWithChecks => {
|
||||
const toStringId = (value: unknown): string => {
|
||||
if (value instanceof mongoose.Types.ObjectId) {
|
||||
return value.toString();
|
||||
}
|
||||
return value?.toString() ?? "";
|
||||
};
|
||||
|
||||
const toDateString = (value: Date | string): string => {
|
||||
if (!value) return "";
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
};
|
||||
|
||||
const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification));
|
||||
|
||||
const checks: Check[] = (doc.checks ?? []).map((check: any) => ({
|
||||
id: toStringId(check._id),
|
||||
metadata: {
|
||||
monitorId: toStringId(check.metadata?.monitorId),
|
||||
teamId: toStringId(check.metadata?.teamId),
|
||||
type: check.metadata?.type,
|
||||
},
|
||||
status: check.status,
|
||||
responseTime: check.responseTime,
|
||||
timings: check.timings,
|
||||
statusCode: check.statusCode,
|
||||
message: check.message,
|
||||
ack: check.ack,
|
||||
ackAt: check.ackAt ?? null,
|
||||
expiry: toDateString(check.expiry),
|
||||
cpu: check.cpu,
|
||||
memory: check.memory,
|
||||
disk: check.disk,
|
||||
host: check.host,
|
||||
errors: check.errors,
|
||||
capture: check.capture,
|
||||
net: check.net,
|
||||
accessibility: check.accessibility,
|
||||
bestPractices: check.bestPractices,
|
||||
seo: check.seo,
|
||||
performance: check.performance,
|
||||
audits: check.audits,
|
||||
__v: check.__v,
|
||||
createdAt: toDateString(check.createdAt),
|
||||
updatedAt: toDateString(check.updatedAt),
|
||||
}));
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
userId: toStringId(doc.userId),
|
||||
teamId: toStringId(doc.teamId),
|
||||
name: doc.name,
|
||||
description: doc.description ?? undefined,
|
||||
status: doc.status ?? undefined,
|
||||
statusWindow: doc.statusWindow ?? [],
|
||||
statusWindowSize: doc.statusWindowSize,
|
||||
statusWindowThreshold: doc.statusWindowThreshold,
|
||||
type: doc.type,
|
||||
ignoreTlsErrors: doc.ignoreTlsErrors,
|
||||
jsonPath: doc.jsonPath ?? undefined,
|
||||
expectedValue: doc.expectedValue ?? undefined,
|
||||
matchMethod: doc.matchMethod ?? undefined,
|
||||
url: doc.url,
|
||||
port: doc.port ?? undefined,
|
||||
isActive: doc.isActive,
|
||||
interval: doc.interval,
|
||||
uptimePercentage: doc.uptimePercentage ?? undefined,
|
||||
notifications: notificationIds,
|
||||
secret: doc.secret ?? undefined,
|
||||
thresholds: doc.thresholds ?? undefined,
|
||||
alertThreshold: doc.alertThreshold,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
memoryAlertThreshold: doc.memoryAlertThreshold,
|
||||
diskAlertThreshold: doc.diskAlertThreshold,
|
||||
tempAlertThreshold: doc.tempAlertThreshold,
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
checks,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorsRepository;
|
||||
@@ -2,10 +2,15 @@ import type { StatusPage } from "@/types/statusPage.js";
|
||||
|
||||
export interface IStatusPagesRepository {
|
||||
// create
|
||||
create(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
|
||||
// single fetch
|
||||
findByUrl(url: string): Promise<StatusPage>;
|
||||
findByTeamId(teamId: string): Promise<StatusPage[]>;
|
||||
// collection fetch
|
||||
// update
|
||||
updateById(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
|
||||
// delete
|
||||
deleteById(id: string, teamId: string): Promise<StatusPage>;
|
||||
// other
|
||||
removeMonitorFromStatusPages(monitorId: string): Promise<number>;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { IStatusPagesRepository } from "@/repositories/index.js";
|
||||
import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js";
|
||||
import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js";
|
||||
import mongoose from "mongoose";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
class MongoStatusPagesRepository implements IStatusPagesRepository {
|
||||
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
|
||||
@@ -56,6 +57,74 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
|
||||
};
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: StatusPageDocument[]): StatusPage[] => {
|
||||
if (!documents?.length) {
|
||||
return [];
|
||||
}
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
|
||||
const statusPage = new StatusPageModel({
|
||||
...data,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
if (image) {
|
||||
statusPage.logo = {
|
||||
data: image.buffer,
|
||||
contentType: image.mimetype,
|
||||
};
|
||||
}
|
||||
await statusPage.save();
|
||||
return this.toEntity(statusPage);
|
||||
};
|
||||
|
||||
findByUrl = async (url: string): Promise<StatusPage> => {
|
||||
const statusPage = await StatusPageModel.findOne({
|
||||
url,
|
||||
});
|
||||
if (!statusPage) {
|
||||
throw new AppError({ message: "Status page not found", status: 404 });
|
||||
}
|
||||
return this.toEntity(statusPage);
|
||||
// Get status page
|
||||
};
|
||||
|
||||
findByTeamId = async (teamId: string): Promise<StatusPage[]> => {
|
||||
const statusPages = await StatusPageModel.find({ teamId });
|
||||
return this.mapDocuments(statusPages);
|
||||
};
|
||||
|
||||
updateById = async (id: string, teamId: string, image: Express.Multer.File | undefined, patch: Partial<StatusPage>): Promise<StatusPage> => {
|
||||
if (image) {
|
||||
patch.logo = {
|
||||
data: image.buffer,
|
||||
contentType: image.mimetype,
|
||||
};
|
||||
} else {
|
||||
patch.logo = null;
|
||||
}
|
||||
|
||||
const statusPage = await StatusPageModel.findOneAndUpdate({ teamId, _id: id }, patch, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (!statusPage) {
|
||||
throw new AppError({ message: "Status page not found", status: 404 });
|
||||
}
|
||||
|
||||
return this.toEntity(statusPage);
|
||||
};
|
||||
|
||||
deleteById = async (id: string, teamId: string): Promise<StatusPage> => {
|
||||
const statusPage = await StatusPageModel.findOneAndDelete({ _id: id, teamId });
|
||||
if (!statusPage) {
|
||||
throw new AppError({ message: "Status page not found", status: 404 });
|
||||
}
|
||||
return this.toEntity(statusPage);
|
||||
};
|
||||
|
||||
removeMonitorFromStatusPages = async (monitorId: string): Promise<number> => {
|
||||
const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } });
|
||||
return res.modifiedCount;
|
||||
|
||||
@@ -13,11 +13,10 @@ class StatusPageRoutes {
|
||||
}
|
||||
|
||||
initRoutes(verifyJWT: RequestHandler) {
|
||||
this.router.get("/", this.statusPageController.getStatusPage);
|
||||
this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId);
|
||||
|
||||
this.router.post("/", upload.single("logo"), verifyJWT, this.statusPageController.createStatusPage);
|
||||
this.router.put("/", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage);
|
||||
this.router.put("/:id", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage);
|
||||
|
||||
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
|
||||
this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type IStatusPagesRepository } from "@/repositories/index.js";
|
||||
import { StatusPage } from "@/types/index.js";
|
||||
export interface IStatusPageService {
|
||||
createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
|
||||
getStatusPageByUrl(url: string): Promise<StatusPage>;
|
||||
getStatusPagesByTeamId(teamId: string): Promise<StatusPage[]>;
|
||||
updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
|
||||
|
||||
deleteStatusPage(statusPageId: string, teamId: string): Promise<StatusPage>;
|
||||
}
|
||||
|
||||
export class StatusPageService implements IStatusPageService {
|
||||
private statusPagesRepository: IStatusPagesRepository;
|
||||
constructor(statusPagesRepository: IStatusPagesRepository) {
|
||||
this.statusPagesRepository = statusPagesRepository;
|
||||
}
|
||||
|
||||
createStatusPage = async (
|
||||
userId: string,
|
||||
teamId: string,
|
||||
image: Express.Multer.File | undefined,
|
||||
data: Partial<StatusPage>
|
||||
): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.create(userId, teamId, image, data);
|
||||
};
|
||||
|
||||
getStatusPageByUrl = async (url: string): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.findByUrl(url);
|
||||
};
|
||||
|
||||
getStatusPagesByTeamId = async (teamId: string): Promise<StatusPage[]> => {
|
||||
return await this.statusPagesRepository.findByTeamId(teamId);
|
||||
};
|
||||
|
||||
updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.updateById(id, teamId, image, data);
|
||||
};
|
||||
|
||||
deleteStatusPage = async (statusPageId: string, teamId: string): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.deleteById(statusPageId, teamId);
|
||||
};
|
||||
}
|
||||
@@ -11,3 +11,4 @@ export * from "@/service/infrastructure/notificationsService.js";
|
||||
export * from "@/service/infrastructure/statusService.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
||||
export * from "@/service/business/statusPageService.js";
|
||||
@@ -1,6 +1,8 @@
|
||||
import { InitializedServices } from "./config/services.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import type { Server } from "http";
|
||||
|
||||
export const initShutdownListener = (server, services) => {
|
||||
export const initShutdownListener = (server: Server, services: InitializedServices) => {
|
||||
const SERVICE_NAME = "Server";
|
||||
|
||||
let isShuttingDown = false;
|
||||
@@ -19,11 +21,12 @@ export const initShutdownListener = (server, services) => {
|
||||
logger.info({ message: "Graceful shutdown complete" });
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error({
|
||||
message: error.message,
|
||||
message: err.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "shutdown",
|
||||
stack: error.stack,
|
||||
stack: err.stack,
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Check } from "@/types/check.js";
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
@@ -53,7 +54,7 @@ export interface MonitorsSummary {
|
||||
}
|
||||
|
||||
export interface MonitorWithChecks extends Monitor {
|
||||
checks: import("./check.js").Check[];
|
||||
checks: Check[];
|
||||
}
|
||||
|
||||
export interface MonitorsWithChecksByTeamIdResult {
|
||||
|
||||
Reference in new issue
Block a user