From cc1814f507041bb0f64845bed5d5442c21e920f2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jul 2026 09:47:29 -0700 Subject: [PATCH] whitelist monitor properties instead of blacklist for status pages --- .../api/controllers/statusPageController.ts | 49 +--------------- server/src/config/controllers.ts | 2 +- server/src/config/services.api.ts | 2 +- .../status-pages/status-page.service.ts | 58 ++++++++++++++++--- .../domain/status-pages/status-page.type.ts | 10 ++++ 5 files changed, 66 insertions(+), 55 deletions(-) diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index 394d76e23..d820e5eed 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -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, diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 7646b41db..5b7d3c3f7 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -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), diff --git a/server/src/config/services.api.ts b/server/src/config/services.api.ts index 0b9361e5d..2768770c3 100644 --- a/server/src/config/services.api.ts +++ b/server/src/config/services.api.ts @@ -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); diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index 6be395e49..f7827041a 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -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): Promise; getStatusPageByUrl(url: string): Promise; getStatusPageByCustomDomain(customDomain: string): Promise; getStatusPagesByTeamId(teamId: string): Promise; + getPublicStatusPagePayload(statusPage: StatusPage, requesterTeamId: string | undefined): Promise; updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; deleteStatusPage(statusPageId: string, teamId: string): Promise; } 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): Partial => 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 => { + 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): Promise => { const normalizedData = this.normalizeCustomDomainInput(this.normalizeInput(data)); const updated = await this.statusPagesRepository.updateById(id, teamId, image, normalizedData); diff --git a/server/src/domain/status-pages/status-page.type.ts b/server/src/domain/status-pages/status-page.type.ts index 254a411e1..d4642af17 100644 --- a/server/src/domain/status-pages/status-page.type.ts +++ b/server/src/domain/status-pages/status-page.type.ts @@ -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 & + Partial> & { checks: NormalizedCheck[] }; + +export interface PublicStatusPagePayload { + statusPage: StatusPage; + monitors: PublicStatusPageMonitor[]; +}