whitelist monitor properties instead of blacklist for status pages

This commit is contained in:
Alex Holliday committed 2026-07-07 09:47:29 -07:00
1 parent 436c11bcba
commit cc1814f507
5 files changed
+66 -55

No files matched your search

@@ -10,11 +10,7 @@ import {
import { AppError } from "@/utils/AppError.js";
import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.js";
import { IStatusPageService } from "@/domain/status-pages/status-page.service.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { NormalizeData } from "@/utils/dataUtils.js";
import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js";
import type { StatusPage } from "@/domain/status-pages/status-page.type.js";
const SERVICE_NAME = "statusPageController";
@@ -31,53 +27,14 @@ export interface IStatusPageController {
class StatusPageController implements IStatusPageController {
static SERVICE_NAME = SERVICE_NAME;
private statusPageService: IStatusPageService;
private monitorsRepository: IMonitorsRepository;
private settingsService: ISettingsService;
constructor(statusPageService: IStatusPageService, monitorsRepository: IMonitorsRepository, settingsService: ISettingsService) {
constructor(statusPageService: IStatusPageService) {
this.statusPageService = statusPageService;
this.monitorsRepository = monitorsRepository;
this.settingsService = settingsService;
}
get serviceName() {
return StatusPageController.SERVICE_NAME;
}
private buildStatusPagePayload = async (statusPage: StatusPage, req: Request) => {
if (!statusPage.isPublished) {
const teamId = requireTeamId(req?.user?.teamId);
if (statusPage.teamId !== teamId) {
throw new AppError({ message: "Forbidden", status: 403 });
}
}
const settings = await this.settingsService.getDBSettings();
const showURL = settings.showURL;
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
const monitorOrder = new Map(statusPage.monitors.map((id, index) => [id, index]));
const sortedMonitors = [...monitors].sort((a, b) => {
const orderA = monitorOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
const orderB = monitorOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
const normalizedMonitors = sortedMonitors.map((monitor) => {
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
if (!showURL) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { url, port, secret, notifications, ...rest } = monitor;
return { ...rest, checks: normalizedChecks };
}
return { ...monitor, checks: normalizedChecks };
});
return {
statusPage,
monitors: normalizedMonitors,
};
};
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
try {
createStatusPageBodyValidation.parse(req.body);
@@ -135,7 +92,7 @@ class StatusPageController implements IStatusPageController {
}
const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url as string);
const data = await this.buildStatusPagePayload(statusPage, req);
const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId);
return res.status(200).json({
success: true,
@@ -161,7 +118,7 @@ class StatusPageController implements IStatusPageController {
throw new AppError({ message: "Status page not found", status: 404 });
}
const data = await this.buildStatusPagePayload(statusPage, req);
const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId);
return res.status(200).json({
success: true,
+1 -1
View File
@@ -41,7 +41,7 @@ export const initializeControllers = (apiServices: ApiServices): InitializedCont
maintenanceWindowController: new MaintenanceWindowController(apiServices.maintenanceWindowService),
queueController: new QueueController(apiServices.worker),
logController: new LogController(apiServices.logger),
statusPageController: new StatusPageController(apiServices.statusPageService, apiServices.monitorsRepository, apiServices.settingsService),
statusPageController: new StatusPageController(apiServices.statusPageService),
notificationController: new NotificationController(apiServices.notificationsService, apiServices.monitorsRepository),
tagController: new TagsController(apiServices.tagsService),
diagnosticController: new DiagnosticController(apiServices.diagnosticService),
+1 -1
View File
@@ -91,7 +91,7 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A
emailService,
});
const statusPageService = new StatusPageService(statusPagesRepository, settingsService);
const statusPageService = new StatusPageService(statusPagesRepository, settingsService, monitorsRepository);
const tagsService = new TagsService(tagsRepository, monitorsRepository);
const diagnosticService = new DiagnosticService(db);
@@ -1,26 +1,34 @@
import { type IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, StatusPage } from "@/domain/status-pages/status-page.type.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import {
DEFAULT_STATUS_PAGE_THEME,
DEFAULT_STATUS_PAGE_THEME_MODE,
PublicStatusPagePayload,
StatusPage,
} from "@/domain/status-pages/status-page.type.js";
import { AppError } from "@/utils/AppError.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { Monitor } from "@/domain/monitors/monitor.types.js";
import { NormalizeData } from "@/utils/dataUtils.js";
export interface IStatusPageService {
createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
getStatusPageByUrl(url: string): Promise<StatusPage>;
getStatusPageByCustomDomain(customDomain: string): Promise<StatusPage>;
getStatusPagesByTeamId(teamId: string): Promise<StatusPage[]>;
getPublicStatusPagePayload(statusPage: StatusPage, requesterTeamId: string | undefined): Promise<PublicStatusPagePayload>;
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;
private settingsService: ISettingsService;
constructor(statusPagesRepository: IStatusPagesRepository, settingsService: ISettingsService) {
this.statusPagesRepository = statusPagesRepository;
this.settingsService = settingsService;
}
constructor(
private statusPagesRepository: IStatusPagesRepository,
private settingsService: ISettingsService,
private monitorsRepository: IMonitorsRepository
) {}
private assertCustomDomainAllowed = (customDomain: string | null | undefined) => {
if (!customDomain) {
@@ -63,6 +71,27 @@ export class StatusPageService implements IStatusPageService {
private normalizeInput = (data: Partial<StatusPage>): Partial<StatusPage> =>
this.settingsService.areStatusPageThemesEnabled() ? data : this.withoutThemeFields(data);
private toPublicMonitor = (monitor: Monitor, showURL: boolean) => {
const base = {
id: monitor.id,
name: monitor.name,
type: monitor.type,
status: monitor.status,
uptimePercentage: monitor.uptimePercentage,
recentChecks: monitor.recentChecks,
checks: NormalizeData(monitor.recentChecks, 10, 100),
};
if (showURL) {
return {
...base,
url: monitor.url,
port: monitor.port,
};
}
return base;
};
createStatusPage = async (
userId: string,
teamId: string,
@@ -89,6 +118,21 @@ export class StatusPageService implements IStatusPageService {
return statusPages.map((sp) => this.normalizeTheme(sp));
};
getPublicStatusPagePayload = async (statusPage: StatusPage, requesterTeamId: string | undefined): Promise<PublicStatusPagePayload> => {
if (!statusPage.isPublished) {
if (!requesterTeamId || statusPage.teamId !== requesterTeamId) {
throw new AppError({ message: "Forbidden", status: 403 });
}
}
const showURL = (await this.settingsService.getDBSettings()).showURL;
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
const order = new Map(statusPage.monitors.map((id, i) => [id, i]));
const sorted = [...monitors].sort((a, b) => (order.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.id) ?? Number.MAX_SAFE_INTEGER));
return { statusPage, monitors: sorted.map((monitor) => this.toPublicMonitor(monitor, showURL)) };
};
updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
const normalizedData = this.normalizeCustomDomainInput(this.normalizeInput(data));
const updated = await this.statusPagesRepository.updateById(id, teamId, image, normalizedData);
@@ -1,3 +1,5 @@
import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { NormalizedCheck, CheckSnapshot } from "@/domain/checks/check.type.js";
export const StatusPageTypes = ["uptime", "infrastructure"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
@@ -44,3 +46,11 @@ export interface StatusPage {
createdAt: string;
updatedAt: string;
}
export type PublicStatusPageMonitor = Pick<Monitor, "id" | "name" | "type" | "status" | "uptimePercentage" | "recentChecks"> &
Partial<Pick<Monitor, "url" | "port">> & { checks: NormalizedCheck<CheckSnapshot>[] };
export interface PublicStatusPagePayload {
statusPage: StatusPage;
monitors: PublicStatusPageMonitor[];
}