diff --git a/server/src/api/routes/proxyRoutes.ts b/server/src/api/routes/proxyRoutes.ts index d03661fbc..83bf6a318 100644 --- a/server/src/api/routes/proxyRoutes.ts +++ b/server/src/api/routes/proxyRoutes.ts @@ -1,12 +1,12 @@ import { IProxiesController } from "@/api/controllers/proxyController.js"; import { Router } from "express"; -export const createProxyRoutes = (tagController: IProxiesController): Router => { +export const createProxyRoutes = (proxyController: IProxiesController): Router => { const router = Router(); - router.post("/", tagController.createProxy); - router.get("/team", tagController.getProxiesByTeamId); - router.get("/:id", tagController.getProxyById); - router.delete("/:id", tagController.deleteProxy); - router.patch("/:id", tagController.editProxy); + router.post("/", proxyController.createProxy); + router.get("/team", proxyController.getProxiesByTeamId); + router.get("/:id", proxyController.getProxyById); + router.delete("/:id", proxyController.deleteProxy); + router.patch("/:id", proxyController.editProxy); return router; }; diff --git a/server/src/api/validation/proxyValidation.ts b/server/src/api/validation/proxyValidation.ts index 7e79a2542..a2e3edfa0 100644 --- a/server/src/api/validation/proxyValidation.ts +++ b/server/src/api/validation/proxyValidation.ts @@ -14,7 +14,10 @@ export const createProxyBodyValidation = z.object({ password: z.string().optional(), }); -export const editProxyBodyValidation = createProxyBodyValidation; +// Empty/absent password keeps doesn't delete, only clearPassword: true does. +export const editProxyBodyValidation = createProxyBodyValidation.extend({ + clearPassword: z.boolean().optional(), +}); export const getProxyByIdParamValidation = z.object({ id: z.string().min(1, "Proxy ID is required"), @@ -26,8 +29,6 @@ export const deleteProxyParamValidation = z.object({ id: z.string().min(1, "Proxy ID is required"), }); -// Canonical proxy shape returned by /proxies endpoints. Keep aligned with what -// the controllers actually serialize (password is intentionally omitted; hasPassword tells the client one is stored). export const proxyResponseSchema = z .object({ id: z.string(), diff --git a/server/src/domain/proxies/proxy.repository.interface.ts b/server/src/domain/proxies/proxy.repository.interface.ts index bfc2e3ab1..17998392c 100644 --- a/server/src/domain/proxies/proxy.repository.interface.ts +++ b/server/src/domain/proxies/proxy.repository.interface.ts @@ -7,7 +7,7 @@ export interface IProxiesRepository { findById(proxyId: string, teamId: string): Promise; findByTeamId(teamId: string): Promise; // update - updateById(proxyId: string, teamId: string, patch: Partial): Promise; + updateById(proxyId: string, teamId: string, patch: Partial, options?: { unsetPassword?: boolean }): Promise; // delete deleteById(proxyId: string, teamId: string): Promise; } diff --git a/server/src/domain/proxies/proxy.repository.mongo.ts b/server/src/domain/proxies/proxy.repository.mongo.ts index 8890fe66f..350411d7a 100644 --- a/server/src/domain/proxies/proxy.repository.mongo.ts +++ b/server/src/domain/proxies/proxy.repository.mongo.ts @@ -3,6 +3,7 @@ import { Proxy } from "@/domain/proxies/proxy.type.js"; import { ProxyDocument, ProxyModel } from "@/domain/proxies/proxy.model.js"; import { AppError } from "@/utils/AppError.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; +import { UpdateQuery } from "mongoose"; const SERVICE_NAME = "ProxiesRepository"; @@ -49,17 +50,18 @@ class MongoProxiesRepository implements IProxiesRepository { return proxies.map(this.toEntity); } - async updateById(proxyId: string, teamId: string, patch: Partial): Promise { + async updateById(proxyId: string, teamId: string, patch: Partial, options?: { unsetPassword?: boolean }): Promise { try { - const updatedProxy = await ProxyModel.findOneAndUpdate( - { _id: proxyId, teamId }, - { - $set: { - ...patch, - }, + const update: UpdateQuery = { + $set: { + ...patch, }, - { new: true, runValidators: true } - ); + }; + if (options?.unsetPassword) { + delete update.$set?.password; + update.$unset = { password: 1 }; + } + const updatedProxy = await ProxyModel.findOneAndUpdate({ _id: proxyId, teamId }, update, { new: true, runValidators: true }); if (!updatedProxy) { throw new AppError({ message: "Proxy not found or could not be updated", service: SERVICE_NAME, status: 404 }); } diff --git a/server/src/domain/proxies/proxy.service.ts b/server/src/domain/proxies/proxy.service.ts index 97a071b59..a57e3bfd5 100644 --- a/server/src/domain/proxies/proxy.service.ts +++ b/server/src/domain/proxies/proxy.service.ts @@ -1,15 +1,15 @@ -import { Proxy } from "@/domain/proxies/proxy.type.js"; +import { Proxy, ProxyResponse } 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; - getProxy(proxyId: string, teamId: string): Promise; - getProxiesByTeamId(teamId: string): Promise; - updateProxy(proxyId: string, teamId: string, patch: Partial): Promise; - deleteProxy(proxyId: string, teamId: string): Promise; + createProxy(proxy: Partial, teamId: string): Promise; + getProxy(proxyId: string, teamId: string): Promise; + getProxiesByTeamId(teamId: string): Promise; + updateProxy(proxyId: string, teamId: string, patch: Partial & { clearPassword?: boolean }): Promise; + deleteProxy(proxyId: string, teamId: string): Promise; } const SERVICE_NAME = "ProxiesService"; @@ -20,23 +20,46 @@ export class ProxiesService implements IProxiesService { private settingsService: ISettingsService ) {} - createProxy = async (proxyData: Partial, teamId: string): Promise => { + private toResponse = ({ password, ...rest }: Proxy): ProxyResponse => ({ ...rest, hasPassword: Boolean(password) }); + + createProxy = async (proxyData: Partial, teamId: string): Promise => { proxyData.teamId = teamId; - return await this.proxiesRepository.create(proxyData); + if (!proxyData.password) { + delete proxyData.password; + } + if (!proxyData.username) { + delete proxyData.username; + } + const raw = await this.proxiesRepository.create(proxyData); + return this.toResponse(raw); }; - getProxy = async (proxyId: string, teamId: string): Promise => { - return await this.proxiesRepository.findById(proxyId, teamId); + getProxy = async (proxyId: string, teamId: string): Promise => { + const raw = await this.proxiesRepository.findById(proxyId, teamId); + return this.toResponse(raw); }; - getProxiesByTeamId = async (teamId: string): Promise => { - return await this.proxiesRepository.findByTeamId(teamId); + getProxiesByTeamId = async (teamId: string): Promise => { + const raw = await this.proxiesRepository.findByTeamId(teamId); + const clean = raw.map((r) => { + return this.toResponse(r); + }); + return clean; }; - updateProxy = async (proxyId: string, teamId: string, patch: Partial): Promise => { - return await this.proxiesRepository.updateById(proxyId, teamId, patch); + updateProxy = async (proxyId: string, teamId: string, patch: Partial & { clearPassword?: boolean }): Promise => { + // Empty/absent password keeps the stored one; clearPassword removes it + const { clearPassword, ...proxyPatch } = patch; + if (!proxyPatch.password) { + delete proxyPatch.password; + } + if (!proxyPatch.username) { + delete proxyPatch.username; + } + const raw = await this.proxiesRepository.updateById(proxyId, teamId, proxyPatch, { unsetPassword: clearPassword === true }); + return this.toResponse(raw); }; - deleteProxy = async (proxyId: string, teamId: string): Promise => { + deleteProxy = async (proxyId: string, teamId: string): Promise => { // Make sure proxy is not in use const monitorsUsingProxy = await this.monitorsRepository.findMonitorCountByProxyId(proxyId); if (monitorsUsingProxy > 0) { @@ -56,6 +79,7 @@ export class ProxiesService implements IProxiesService { status: 409, }); } - return await this.proxiesRepository.deleteById(proxyId, teamId); + const raw = await this.proxiesRepository.deleteById(proxyId, teamId); + return this.toResponse(raw); }; } diff --git a/server/src/domain/proxies/proxy.type.ts b/server/src/domain/proxies/proxy.type.ts index a21ab9875..dd098dee3 100644 --- a/server/src/domain/proxies/proxy.type.ts +++ b/server/src/domain/proxies/proxy.type.ts @@ -13,3 +13,5 @@ export interface Proxy { createdAt: string; updatedAt: string; } + +export type ProxyResponse = Omit & { hasPassword: boolean };