mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
- Replaced `forwardAuthProviderId` with `forwardAuthEnabled` in the domain schema to simplify the configuration of forward authentication. - Updated related tests to reflect this change, ensuring consistency across the application. - Introduced a new SQL migration to create the `forward_auth_settings` table for managing authentication domains and their configurations. This refactor enhances the clarity and maintainability of the forward authentication logic within the application.
225 lines
6.8 KiB
TypeScript
225 lines
6.8 KiB
TypeScript
import type { Domain } from "@dokploy/server/services/domain";
|
|
import type { ApplicationNested } from "../builders";
|
|
import {
|
|
createServiceConfig,
|
|
loadOrCreateConfig,
|
|
loadOrCreateConfigRemote,
|
|
removeTraefikConfig,
|
|
removeTraefikConfigRemote,
|
|
writeTraefikConfig,
|
|
writeTraefikConfigRemote,
|
|
} from "./application";
|
|
import type { FileConfig, HttpRouter } from "./file-types";
|
|
import {
|
|
createForwardAuthMiddleware,
|
|
forwardAuthMiddlewareName,
|
|
removeForwardAuthMiddleware,
|
|
} from "./forward-auth";
|
|
import { createPathMiddlewares, removePathMiddlewares } from "./middleware";
|
|
|
|
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
|
const { appName } = app;
|
|
let config: FileConfig;
|
|
|
|
if (app.serverId) {
|
|
config = await loadOrCreateConfigRemote(app.serverId, appName);
|
|
} else {
|
|
config = loadOrCreateConfig(appName);
|
|
}
|
|
const serviceName = `${appName}-service-${domain.uniqueConfigKey}`;
|
|
const routerName = `${appName}-router-${domain.uniqueConfigKey}`;
|
|
const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`;
|
|
|
|
config.http = config.http || { routers: {}, services: {} };
|
|
config.http.routers = config.http.routers || {};
|
|
config.http.services = config.http.services || {};
|
|
|
|
config.http.routers[routerName] = await createRouterConfig(
|
|
app,
|
|
domain,
|
|
domain.customEntrypoint || "web",
|
|
);
|
|
|
|
if (!domain.customEntrypoint && domain.https) {
|
|
config.http.routers[routerNameSecure] = await createRouterConfig(
|
|
app,
|
|
domain,
|
|
"websecure",
|
|
);
|
|
} else {
|
|
delete config.http.routers[routerNameSecure];
|
|
}
|
|
|
|
config.http.services[serviceName] = createServiceConfig(appName, domain);
|
|
|
|
await createPathMiddlewares(app, domain);
|
|
// SSO forward-auth: writes the per-app forwardAuth + errors middlewares (the
|
|
// /oauth2/* router lives on the central auth domain, not here). No-op unless
|
|
// the domain links a provider and the org has an auth domain configured.
|
|
await createForwardAuthMiddleware(app, domain);
|
|
|
|
if (app.serverId) {
|
|
await writeTraefikConfigRemote(config, appName, app.serverId);
|
|
} else {
|
|
writeTraefikConfig(config, appName);
|
|
}
|
|
};
|
|
|
|
export const removeDomain = async (
|
|
application: ApplicationNested,
|
|
uniqueKey: number,
|
|
) => {
|
|
const { appName, serverId } = application;
|
|
let config: FileConfig;
|
|
|
|
if (serverId) {
|
|
config = await loadOrCreateConfigRemote(serverId, appName);
|
|
} else {
|
|
config = loadOrCreateConfig(appName);
|
|
}
|
|
|
|
const routerKey = `${appName}-router-${uniqueKey}`;
|
|
const routerSecureKey = `${appName}-router-websecure-${uniqueKey}`;
|
|
|
|
const serviceKey = `${appName}-service-${uniqueKey}`;
|
|
if (config.http?.routers?.[routerKey]) {
|
|
delete config.http.routers[routerKey];
|
|
}
|
|
if (config.http?.routers?.[routerSecureKey]) {
|
|
delete config.http.routers[routerSecureKey];
|
|
}
|
|
if (config.http?.services?.[serviceKey]) {
|
|
delete config.http.services[serviceKey];
|
|
}
|
|
|
|
await removePathMiddlewares(application, uniqueKey);
|
|
await removeForwardAuthMiddleware(application, uniqueKey);
|
|
|
|
// verify if is the last router if so we delete the router
|
|
if (
|
|
config?.http?.routers &&
|
|
Object.keys(config?.http?.routers).length === 0
|
|
) {
|
|
if (serverId) {
|
|
await removeTraefikConfigRemote(appName, serverId);
|
|
} else {
|
|
await removeTraefikConfig(appName);
|
|
}
|
|
} else {
|
|
if (serverId) {
|
|
await writeTraefikConfigRemote(config, appName, serverId);
|
|
} else {
|
|
writeTraefikConfig(config, appName);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Converts an internationalized domain name (IDN) to ASCII punycode format.
|
|
* Traefik requires domain names in ASCII format, so non-ASCII characters
|
|
* must be converted (e.g., "тест.рф" → "xn--e1aybc.xn--p1ai").
|
|
*/
|
|
const toPunycode = (host: string): string => {
|
|
try {
|
|
return new URL(`http://${host}`).hostname;
|
|
} catch {
|
|
// If URL parsing fails, return the original host
|
|
return host;
|
|
}
|
|
};
|
|
|
|
export const createRouterConfig = async (
|
|
app: ApplicationNested,
|
|
domain: Domain,
|
|
entryPoint: string,
|
|
) => {
|
|
const { appName, redirects, security } = app;
|
|
const { certificateType } = domain;
|
|
|
|
const {
|
|
host,
|
|
path,
|
|
https,
|
|
uniqueConfigKey,
|
|
internalPath,
|
|
stripPath,
|
|
customEntrypoint,
|
|
} = domain;
|
|
const punycodeHost = toPunycode(host);
|
|
const routerConfig: HttpRouter = {
|
|
rule: `Host(\`${punycodeHost}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
|
service: `${appName}-service-${uniqueConfigKey}`,
|
|
middlewares: [],
|
|
entryPoints: [entryPoint],
|
|
};
|
|
|
|
const isRedirectRouter = entryPoint === "web" && https && !customEntrypoint;
|
|
|
|
// Web router with HTTPS only needs redirect — all other middlewares
|
|
// run on the websecure router where the request actually lands.
|
|
if (isRedirectRouter) {
|
|
routerConfig.middlewares?.push("redirect-to-https");
|
|
} else {
|
|
// Add path rewriting middleware if needed
|
|
// stripPrefix must come before addPrefix so Traefik strips the
|
|
// public path first, then prepends the internal path.
|
|
if (stripPath && path && path !== "/") {
|
|
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
|
routerConfig.middlewares?.push(stripMiddleware);
|
|
}
|
|
|
|
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
|
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
|
routerConfig.middlewares?.push(pathMiddleware);
|
|
}
|
|
|
|
// redirects - skip for preview deployments as wildcard subdomains
|
|
// should not inherit parent redirect rules (e.g., www-redirect)
|
|
if (domain.domainType !== "preview") {
|
|
for (const redirect of redirects) {
|
|
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
|
|
routerConfig.middlewares?.push(middlewareName);
|
|
}
|
|
}
|
|
|
|
// security
|
|
if (security.length > 0) {
|
|
let middlewareName = `auth-${appName}`;
|
|
if (domain.domainType === "preview") {
|
|
middlewareName = `auth-${appName.replace(
|
|
/^preview-(.+)-[^-]+$/,
|
|
"$1",
|
|
)}`;
|
|
}
|
|
routerConfig.middlewares?.push(middlewareName);
|
|
}
|
|
|
|
// Enterprise SSO forward-auth gate. Placed before custom middlewares so
|
|
// authentication runs first. No-op unless the domain links a provider.
|
|
// The -errors middleware must come first so a 401 from the auth check is
|
|
// rewritten to a 302 redirect to the login page.
|
|
if (domain.forwardAuthEnabled) {
|
|
const name = forwardAuthMiddlewareName(appName, uniqueConfigKey);
|
|
routerConfig.middlewares?.push(`${name}-errors`);
|
|
routerConfig.middlewares?.push(name);
|
|
}
|
|
|
|
// custom middlewares from domain
|
|
if (domain.middlewares && domain.middlewares.length > 0) {
|
|
routerConfig.middlewares?.push(...domain.middlewares);
|
|
}
|
|
}
|
|
|
|
if (entryPoint === "websecure" || (customEntrypoint && https)) {
|
|
if (certificateType === "letsencrypt") {
|
|
routerConfig.tls = { certResolver: "letsencrypt" };
|
|
} else if (certificateType === "custom" && domain.customCertResolver) {
|
|
routerConfig.tls = { certResolver: domain.customCertResolver };
|
|
} else if (certificateType === "none") {
|
|
routerConfig.tls = undefined;
|
|
}
|
|
}
|
|
|
|
return routerConfig;
|
|
};
|