diff --git a/server/src/config/services.api.ts b/server/src/config/services.api.ts index ae16814fd..5ce76aa3d 100644 --- a/server/src/config/services.api.ts +++ b/server/src/config/services.api.ts @@ -6,14 +6,14 @@ import { IStatusPageService, StatusPageService } from "@/domain/status-pages/sta import { ITagsService, TagsService } from "@/domain/tags/tag.service.js"; import { IUserService, UserService } from "@/domain/users/user.service.js"; import { IJobScheduler } from "@/worker/worker.interface.js"; +import { ProxiesService, IProxiesService } from "@/domain/proxies/proxy.service.js"; +import { SharedServices } from "@/config/services.shared.js"; // Third-party import jwt from "jsonwebtoken"; import crypto from "crypto"; import { games } from "gamedig"; -import { SharedServices } from "@/config/services.shared.js"; - export interface ApiServices extends SharedServices { worker: IJobScheduler; // control-plane handle only (DBQueueWorker in all-in-one, bare JobScheduler in API-only) userService: IUserService; @@ -23,6 +23,7 @@ export interface ApiServices extends SharedServices { statusPageService: IStatusPageService; tagsService: ITagsService; diagnosticService: IDiagnosticService; + proxiesService: IProxiesService; } export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): ApiServices => { @@ -45,6 +46,7 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A teamsRepository, maintenanceWindowsRepository, jobsRepository, + proxiesRepository, } = shared; const userService = new UserService({ @@ -94,7 +96,7 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A const statusPageService = new StatusPageService(statusPagesRepository, settingsService, monitorsRepository, checksRepository); const tagsService = new TagsService(tagsRepository, monitorsRepository); const diagnosticService = new DiagnosticService(db); - + const proxiesService = new ProxiesService(proxiesRepository, monitorsRepository, settingsService); return { ...shared, worker: jobScheduler, @@ -105,5 +107,6 @@ export const buildApi = (shared: SharedServices, jobScheduler: IJobScheduler): A statusPageService, tagsService, diagnosticService, + proxiesService, }; }; diff --git a/server/src/config/services.shared.ts b/server/src/config/services.shared.ts index b278f6eab..7ea873c4b 100644 --- a/server/src/config/services.shared.ts +++ b/server/src/config/services.shared.ts @@ -52,6 +52,7 @@ import { IStatusPagesRepository } from "@/domain/status-pages/status-page-reposi import { ITagsRepository } from "@/domain/tags/tag.repository.interface.js"; import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js"; import { IUsersRepository } from "@/domain/users/user.repository.interface.js"; +import { IProxiesRepository } from "@/domain/proxies/proxy.repository.interface.js"; // Mongo repository implementations import MongoSettingsRepository from "@/domain/app-settings/app-settings.repository.mongo.js"; @@ -70,6 +71,7 @@ import MongoStatusPagesRepository from "@/domain/status-pages/status-page-reposi import MongoTagsRepository from "@/domain/tags/tag.repository.mongo.js"; import MongoTeamsRepository from "@/domain/teams/team.repository.model.js"; import MongoUsersRepository from "@/domain/users/user.repository.mongo.js"; +import MongoProxiesRepository from "@/domain/proxies/proxy.repository.mongo.js"; // Shared infrastructure + business services that both the API and the worker process construct. export interface SharedServices { @@ -103,6 +105,7 @@ export interface SharedServices { incidentsRepository: IIncidentsRepository; teamsRepository: ITeamsRepository; maintenanceWindowsRepository: IMaintenanceWindowsRepository; + proxiesRepository: IProxiesRepository; } export const buildShared = async ({ @@ -139,6 +142,7 @@ export const buildShared = async ({ const tagsRepository = new MongoTagsRepository(); const teamsRepository = new MongoTeamsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); + const proxiesRepository = new MongoProxiesRepository(); // Inject settings repository into settings service (now that DB is connected) (settingsService as SettingsService).setRepository(settingsRepository); @@ -221,6 +225,7 @@ export const buildShared = async ({ incidentsRepository, teamsRepository, maintenanceWindowsRepository, + proxiesRepository, }; return sharedServices; }; diff --git a/server/src/domain/monitors/monitor.repository.interface.ts b/server/src/domain/monitors/monitor.repository.interface.ts index 5528c97cf..1ce2b8fa8 100644 --- a/server/src/domain/monitors/monitor.repository.interface.ts +++ b/server/src/domain/monitors/monitor.repository.interface.ts @@ -53,6 +53,7 @@ export interface IMonitorsRepository { // counts findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; + findMonitorCountByProxyId(proxyId: string): Promise; // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index e75b4949a..6cf2e0ecf 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -172,6 +172,11 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; + findMonitorCountByProxyId = async (proxyId: string): Promise => { + const count = await MonitorModel.countDocuments({ proxyId }); + return count; + }; + updateById = async (monitorId: string, teamId: string, patch: Partial) => { const updatedMonitor = await MonitorModel.findOneAndUpdate( { _id: monitorId, teamId }, diff --git a/server/src/domain/proxies/proxy.service.ts b/server/src/domain/proxies/proxy.service.ts index 0c911c0ab..97a071b59 100644 --- a/server/src/domain/proxies/proxy.service.ts +++ b/server/src/domain/proxies/proxy.service.ts @@ -1,6 +1,8 @@ import { Proxy } from "@/domain/proxies/proxy.type.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import { IProxiesRepository } from "@/domain/proxies/proxy.repository.interface.js"; +import { AppError } from "@/utils/AppError.js"; +import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; export interface IProxiesService { createProxy(proxy: Partial, teamId: string): Promise; @@ -10,10 +12,12 @@ export interface IProxiesService { deleteProxy(proxyId: string, teamId: string): Promise; } +const SERVICE_NAME = "ProxiesService"; export class ProxiesService implements IProxiesService { constructor( private proxiesRepository: IProxiesRepository, - private monitorsRepository: IMonitorsRepository + private monitorsRepository: IMonitorsRepository, + private settingsService: ISettingsService ) {} createProxy = async (proxyData: Partial, teamId: string): Promise => { @@ -33,7 +37,25 @@ export class ProxiesService implements IProxiesService { return await this.proxiesRepository.updateById(proxyId, teamId, patch); }; deleteProxy = async (proxyId: string, teamId: string): Promise => { - // TODO guard + // Make sure proxy is not in use + const monitorsUsingProxy = await this.monitorsRepository.findMonitorCountByProxyId(proxyId); + if (monitorsUsingProxy > 0) { + throw new AppError({ + message: `Proxy still in use by ${monitorsUsingProxy} monitor${monitorsUsingProxy > 1 ? "s" : ""}`, + service: SERVICE_NAME, + status: 409, + }); + } + + // Make sure this is not set as a global proxy + const settings = await this.settingsService.getDBSettings(); + if (settings.globalProxyId === proxyId) { + throw new AppError({ + message: "Proxy is set as the global proxy in settings", + service: SERVICE_NAME, + status: 409, + }); + } return await this.proxiesRepository.deleteById(proxyId, teamId); }; }