From fd0f679d0f79ca949c442b1fe33b727efb83a9ef Mon Sep 17 00:00:00 2001 From: Jhonatan Caldeira Date: Sun, 22 Jun 2025 14:55:27 -0300 Subject: [PATCH] feat(domains): add internal path routing and strip path functionality to compose - Add internalPath field to route requests to different paths internally - Add stripPath option to remove external path prefix before forwarding - Improves validation for stripPath (requires non-root path) and internalPath (must start with /) --- .../application/domains/handle-domain.tsx | 18 +++++++++ packages/server/src/db/validations/domain.ts | 40 +++++++++++++++++++ packages/server/src/utils/docker/domain.ts | 22 ++++++++++ 3 files changed, 80 insertions(+) diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx index ff3781da..53f899e9 100644 --- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx @@ -86,6 +86,24 @@ export const domain = z message: "Required", }); } + + // Validate stripPath requires a valid path + if (input.stripPath && (!input.path || input.path === "/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["stripPath"], + message: "Strip path can only be enabled when a path other than '/' is specified", + }); + } + + // Validate internalPath starts with / + if (input.internalPath && input.internalPath !== "/" && !input.internalPath.startsWith("/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["internalPath"], + message: "Internal path must start with '/'", + }); + } }); type Domain = z.infer; diff --git a/packages/server/src/db/validations/domain.ts b/packages/server/src/db/validations/domain.ts index 75dc56ff..6fc74c74 100644 --- a/packages/server/src/db/validations/domain.ts +++ b/packages/server/src/db/validations/domain.ts @@ -4,6 +4,8 @@ export const domain = z .object({ host: z.string().min(1, { message: "Add a hostname" }), path: z.string().min(1).optional(), + internalPath: z.string().optional(), + stripPath: z.boolean().optional(), port: z .number() .min(1, { message: "Port must be at least 1" }) @@ -29,12 +31,32 @@ export const domain = z message: "Required when certificate type is custom", }); } + + // Validate stripPath requires a valid path + if (input.stripPath && (!input.path || input.path === "/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["stripPath"], + message: "Strip path can only be enabled when a path other than '/' is specified", + }); + } + + // Validate internalPath starts with / + if (input.internalPath && input.internalPath !== "/" && !input.internalPath.startsWith("/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["internalPath"], + message: "Internal path must start with '/'", + }); + } }); export const domainCompose = z .object({ host: z.string().min(1, { message: "Host is required" }), path: z.string().min(1).optional(), + internalPath: z.string().optional(), + stripPath: z.boolean().optional(), port: z .number() .min(1, { message: "Port must be at least 1" }) @@ -61,4 +83,22 @@ export const domainCompose = z message: "Required when certificate type is custom", }); } + + // Validate stripPath requires a valid path + if (input.stripPath && (!input.path || input.path === "/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["stripPath"], + message: "Strip path can only be enabled when a path other than '/' is specified", + }); + } + + // Validate internalPath starts with / + if (input.internalPath && input.internalPath !== "/" && !input.internalPath.startsWith("/")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["internalPath"], + message: "Internal path must start with '/'", + }); + } }); diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 4f008397..1739f2f1 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -301,6 +301,8 @@ export const createDomainLabels = ( certificateType, path, customCertResolver, + stripPath, + internalPath } = domain; const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`; const labels = [ @@ -310,6 +312,26 @@ export const createDomainLabels = ( `traefik.http.routers.${routerName}.service=${routerName}`, ]; + // Validate stripPath - it should only be used when path is defined and not "/" + if (stripPath) { + if (!path || path === "/") { + console.warn(`stripPath is enabled but path is not defined or is "/" for domain ${host}`); + } else { + const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`; + labels.push(`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`); + } + } + + // Validate internalPath - ensure it's a valid path format + if (internalPath && internalPath !== "/") { + if (!internalPath.startsWith("/")) { + console.warn(`internalPath "${internalPath}" should start with "/" and not be empty for domain ${host}`); + } else { + const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`; + labels.push(`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`); + } + } + if (entrypoint === "web" && https) { labels.push( `traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,