Files
dokploy/packages/server/src/utils/traefik/domain.ts
Mauricio Siu 1df6774ee8 refactor: update forward authentication handling in domain schema and tests
- 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.
2026-06-06 03:53:45 -06:00

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;
};