mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
223 lines
4.9 KiB
TypeScript
223 lines
4.9 KiB
TypeScript
import { db } from "@dokploy/server/db";
|
|
import { generateRandomDomain } from "@dokploy/server/templates";
|
|
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { eq } from "drizzle-orm";
|
|
import { type apiCreateDomain, domains } from "../db/schema";
|
|
import { findUserById } from "./admin";
|
|
import { findApplicationById } from "./application";
|
|
import { findServerById } from "./server";
|
|
import dns from "node:dns";
|
|
import { promisify } from "node:util";
|
|
|
|
export type Domain = typeof domains.$inferSelect;
|
|
|
|
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
|
const result = await db.transaction(async (tx) => {
|
|
const domain = await tx
|
|
.insert(domains)
|
|
.values({
|
|
...input,
|
|
})
|
|
.returning()
|
|
.then((response) => response[0]);
|
|
|
|
if (!domain) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Error creating domain",
|
|
});
|
|
}
|
|
|
|
if (domain.applicationId) {
|
|
const application = await findApplicationById(domain.applicationId);
|
|
await manageDomain(application, domain);
|
|
}
|
|
|
|
return domain;
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
export const generateTraefikMeDomain = async (
|
|
appName: string,
|
|
userId: string,
|
|
serverId?: string,
|
|
) => {
|
|
if (serverId) {
|
|
const server = await findServerById(serverId);
|
|
return generateRandomDomain({
|
|
serverIp: server.ipAddress,
|
|
projectName: appName,
|
|
});
|
|
}
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
return generateRandomDomain({
|
|
serverIp: "",
|
|
projectName: appName,
|
|
});
|
|
}
|
|
const admin = await findUserById(userId);
|
|
return generateRandomDomain({
|
|
serverIp: admin?.serverIp || "",
|
|
projectName: appName,
|
|
});
|
|
};
|
|
|
|
export const generateWildcardDomain = (
|
|
appName: string,
|
|
serverDomain: string,
|
|
) => {
|
|
return `${appName}-${serverDomain}`;
|
|
};
|
|
|
|
export const findDomainById = async (domainId: string) => {
|
|
const domain = await db.query.domains.findFirst({
|
|
where: eq(domains.domainId, domainId),
|
|
with: {
|
|
application: true,
|
|
},
|
|
});
|
|
if (!domain) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Domain not found",
|
|
});
|
|
}
|
|
return domain;
|
|
};
|
|
|
|
export const findDomainsByApplicationId = async (applicationId: string) => {
|
|
const domainsArray = await db.query.domains.findMany({
|
|
where: eq(domains.applicationId, applicationId),
|
|
with: {
|
|
application: true,
|
|
},
|
|
});
|
|
|
|
return domainsArray;
|
|
};
|
|
|
|
export const findDomainsByComposeId = async (composeId: string) => {
|
|
const domainsArray = await db.query.domains.findMany({
|
|
where: eq(domains.composeId, composeId),
|
|
with: {
|
|
compose: true,
|
|
},
|
|
});
|
|
|
|
return domainsArray;
|
|
};
|
|
|
|
export const updateDomainById = async (
|
|
domainId: string,
|
|
domainData: Partial<Domain>,
|
|
) => {
|
|
const domain = await db
|
|
.update(domains)
|
|
.set({
|
|
...domainData,
|
|
})
|
|
.where(eq(domains.domainId, domainId))
|
|
.returning();
|
|
|
|
return domain[0];
|
|
};
|
|
|
|
export const removeDomainById = async (domainId: string) => {
|
|
await findDomainById(domainId);
|
|
const result = await db
|
|
.delete(domains)
|
|
.where(eq(domains.domainId, domainId))
|
|
.returning();
|
|
|
|
return result[0];
|
|
};
|
|
|
|
export const getDomainHost = (domain: Domain) => {
|
|
return `${domain.https ? "https" : "http"}://${domain.host}`;
|
|
};
|
|
|
|
const resolveDns = promisify(dns.resolve4);
|
|
|
|
// Cloudflare IP ranges (simplified - these are some common ones)
|
|
const CLOUDFLARE_IPS = [
|
|
"172.67.",
|
|
"104.21.",
|
|
"104.16.",
|
|
"104.17.",
|
|
"104.18.",
|
|
"104.19.",
|
|
"104.20.",
|
|
"104.22.",
|
|
"104.23.",
|
|
"104.24.",
|
|
"104.25.",
|
|
"104.26.",
|
|
"104.27.",
|
|
"104.28.",
|
|
];
|
|
|
|
const isCloudflareIp = (ip: string) => {
|
|
return CLOUDFLARE_IPS.some((range) => ip.startsWith(range));
|
|
};
|
|
|
|
export const validateDomain = async (
|
|
domain: string,
|
|
expectedIp?: string,
|
|
): Promise<{
|
|
isValid: boolean;
|
|
resolvedIp?: string;
|
|
error?: string;
|
|
isCloudflare?: boolean;
|
|
}> => {
|
|
try {
|
|
// Remove protocol and path if present
|
|
const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0];
|
|
|
|
// Resolve the domain to get its IP
|
|
const ips = await resolveDns(cleanDomain || "");
|
|
|
|
const resolvedIps = ips.map((ip) => ip.toString());
|
|
|
|
// Check if it's a Cloudflare IP
|
|
const behindCloudflare = ips.some((ip) => isCloudflareIp(ip));
|
|
|
|
// If behind Cloudflare, we consider it valid but inform the user
|
|
if (behindCloudflare) {
|
|
return {
|
|
isValid: true,
|
|
resolvedIp: resolvedIps.join(", "),
|
|
isCloudflare: true,
|
|
error:
|
|
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
|
};
|
|
}
|
|
|
|
// If we have an expected IP, validate against it
|
|
if (expectedIp) {
|
|
return {
|
|
isValid: resolvedIps.includes(expectedIp),
|
|
resolvedIp: resolvedIps.join(", "),
|
|
error: !resolvedIps.includes(expectedIp)
|
|
? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIp}`
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
// If no expected IP, just return the resolved IP
|
|
return {
|
|
isValid: true,
|
|
resolvedIp: resolvedIps.join(", "),
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
isValid: false,
|
|
error:
|
|
error instanceof Error ? error.message : "Failed to resolve domain",
|
|
};
|
|
}
|
|
};
|