diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 6a6ad62ae..f4206ee95 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -191,8 +191,7 @@ export const ShowDomains = ({ id, type }: Props) => { try { const result = await validateDomain({ domain: host, - serverIp: - application?.server?.ipAddress?.toString() || ip?.toString() || "", + serverId: application?.serverId ?? undefined, }); setValidationStates((prev) => ({ diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 505db50c6..10c0b8a8c 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -7,6 +7,7 @@ import { findPreviewDeploymentById, findServerById, generateTraefikMeDomain, + getServerIpCandidates, getWebServerSettings, manageDomain, removeDomain, @@ -248,10 +249,21 @@ export const domainRouter = createTRPCRouter({ .input( z.object({ domain: z.string(), - serverIp: z.string().optional(), + serverId: z.string().optional(), }), ) - .mutation(async ({ input }) => { - return validateDomain(input.domain, input.serverIp); + .mutation(async ({ input, ctx }) => { + if (input.serverId) { + const server = await findServerById(input.serverId); + if (server.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } + } + + const expectedIps = await getServerIpCandidates(input.serverId); + return validateDomain(input.domain, expectedIps); }), }); diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index c651af9fb..a7833341b 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -3,7 +3,9 @@ import { promisify } from "node:util"; import { db } from "@dokploy/server/db"; import { getWebServerSettings } from "@dokploy/server/services/web-server-settings"; import { generateRandomDomain } from "@dokploy/server/templates"; +import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { manageDomain } from "@dokploy/server/utils/traefik/domain"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import type { z } from "zod"; @@ -154,7 +156,7 @@ const resolveDns = promisify(dns.resolve4); export const validateDomain = async ( domain: string, - expectedIp?: string, + expectedIps?: string[], ): Promise<{ isValid: boolean; resolvedIp?: string; @@ -186,13 +188,13 @@ export const validateDomain = async ( }; } - // If we have an expected IP, validate against it - if (expectedIp) { + if (expectedIps && expectedIps.length > 0) { + const isValid = resolvedIps.some((ip) => expectedIps.includes(ip)); return { - isValid: resolvedIps.includes(expectedIp), + isValid, resolvedIp: resolvedIps.join(", "), - error: !resolvedIps.includes(expectedIp) - ? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIp}` + error: !isValid + ? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIps.join(" or ")}` : undefined, }; } @@ -210,3 +212,47 @@ export const validateDomain = async ( }; } }; + +export const getServerIpCandidates = async ( + serverId?: string | null, +): Promise => { + const candidates = new Set(); + + if (serverId) { + const server = await findServerById(serverId); + if (server.ipAddress) { + candidates.add(server.ipAddress); + } + + const publicIp = await withTimeout( + execAsyncRemote( + serverId, + "curl -s -m 5 https://ifconfig.me || curl -s -m 5 https://icanhazip.com", + ), + 7000, + ); + const detectedIp = publicIp?.stdout?.trim(); + if (detectedIp) { + candidates.add(detectedIp); + } + } else { + const settings = await getWebServerSettings(); + if (settings?.serverIp) { + candidates.add(settings.serverIp); + } + + const publicIp = await withTimeout(getPublicIpWithFallback(), 7000); + if (publicIp) { + candidates.add(publicIp); + } + } + + return Array.from(candidates); +}; + +const withTimeout = (promise: Promise, ms: number): Promise => { + return Promise.race([ + promise, + new Promise((resolve) => setTimeout(() => resolve(null), ms)), + ]).catch(() => null); +};