Merge pull request #3174 from bluewave-labs/feat/status-page-service-and-repo

feat/status page service and repo
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-22 13:04:30 -08:00
commit 5292ca8988
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",
+2
View File
@@ -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);
+2 -3
View File
@@ -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({
+2 -2
View File
@@ -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) {
+1 -1
View File
@@ -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),
+5 -6
View File
@@ -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,
};
+51 -30
View File
@@ -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",
+1 -2
View File
@@ -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;
}
-299
View File
@@ -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;
+1 -2
View File
@@ -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);
};
}
+1
View File
@@ -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);
}
+2 -1
View File
@@ -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 {