diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index dcbbefba..5a359d36 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -321,130 +321,49 @@ export async function getTraefikConfig( config_output.http.services = {}; } - const availableServers = (targets as TargetWithSite[]).filter( - (target: TargetWithSite) => { - if (!target.enabled) return false; + const additionalMiddlewares = + config.getRawConfig().traefik.additional_middlewares || []; - const anySitesOnline = (targets as TargetWithSite[]).some( - (t: TargetWithSite) => t.site.online - ); - if (anySitesOnline && !target.site.online) return false; + const routerMiddlewares = [ + badgerMiddlewareName, + ...additionalMiddlewares + ]; - if (target.site.type === "local" || target.site.type === "wireguard") { - return target.ip && target.port && target.method; - } else if (target.site.type === "newt") { - return target.internalPort && target.method && target.site.subnet; - } - return false; - } - ); + let rule = `Host(\`${fullDomain}\`)`; - const hasHealthyServers = availableServers.length > 0; - - let showMaintenancePage = false; - if (resource.maintenanceModeEnabled) { - if (resource.maintenanceModeType === "forced") { - showMaintenancePage = true; - logger.debug( - `Resource ${resource.name} (${fullDomain}) is in FORCED maintenance mode` - ); - } else if (resource.maintenanceModeType === "automatic") { - showMaintenancePage = !hasHealthyServers; - if (showMaintenancePage) { - logger.warn( - `Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)` - ); - } - } - } - - if (showMaintenancePage) { - const maintenanceServiceName = `${key}-maintenance-service`; - const maintenanceRouterName = `${key}-maintenance-router`; - const rewriteMiddlewareName = `${key}-maintenance-rewrite`; - - const entrypointHttp = config.getRawConfig().traefik.http_entrypoint; - const entrypointHttps = config.getRawConfig().traefik.https_entrypoint; - - const fullDomain = resource.fullDomain; - const domainParts = fullDomain.split("."); - const wildCard = resource.subdomain - ? `*.${domainParts.slice(1).join(".")}` - : fullDomain; - - const tls = { - certResolver: resource.domainCertResolver?.trim() || - config.getRawConfig().traefik.cert_resolver, - ...(config.getRawConfig().traefik.prefer_wildcard_cert - ? { domains: [{ main: wildCard }] } - : {}) - }; - - const maintenancePort = config.getRawConfig().server.next_port; - const maintenanceHost = config.getRawConfig().server.internal_hostname; - - config_output.http.services[maintenanceServiceName] = { - loadBalancer: { - servers: [{ url: `http://${maintenanceHost}:${maintenancePort}` }], - passHostHeader: true - } - }; - - // middleware to rewrite path to /maintenance-screen - if (!config_output.http.middlewares) { - config_output.http.middlewares = {}; - } - - config_output.http.middlewares[rewriteMiddlewareName] = { - replacePathRegex: { - regex: "^/(.*)", - replacement: "/maintenance-screen" - } - }; - - - const rule = `Host(\`${fullDomain}\`)`; - - console.log('DEBUG: Generated rule:', rule); // Should show: Host(`pangolin.pallavi.fosrl.io`) - - config_output.http.routers[maintenanceRouterName] = { - entryPoints: [resource.ssl ? entrypointHttps : entrypointHttp], - service: maintenanceServiceName, - middlewares: [rewriteMiddlewareName], - rule: rule, - priority: 2000, - ...(resource.ssl ? { tls } : {}) - }; - - if (resource.ssl) { - config_output.http.routers[`${maintenanceRouterName}-redirect`] = { - entryPoints: [entrypointHttp], - middlewares: [redirectHttpsMiddlewareName, rewriteMiddlewareName], - service: maintenanceServiceName, - rule: rule, - priority: 2000 - }; - } - - logger.info(`Maintenance mode active for ${fullDomain}`); - - continue; - } - - const domainParts = fullDomain.split("."); - let wildCard; - if (domainParts.length <= 2) { - wildCard = `*.${domainParts.join(".")}`; + // priority logic + let priority: number; + if (resource.priority && resource.priority != 100) { + priority = resource.priority; } else { - wildCard = `*.${domainParts.slice(1).join(".")}`; + priority = 100; + if (resource.path && resource.pathMatchType) { + priority += 10; + if (resource.pathMatchType === "exact") { + priority += 5; + } else if (resource.pathMatchType === "prefix") { + priority += 3; + } else if (resource.pathMatchType === "regex") { + priority += 2; + } + if (resource.path === "/") { + priority = 1; // lowest for catch-all + } + } } - if (!resource.subdomain) { - wildCard = resource.fullDomain; + if (resource.ssl) { + config_output.http.routers![routerName + "-redirect"] = { + entryPoints: [ + config.getRawConfig().traefik.http_entrypoint + ], + middlewares: [redirectHttpsMiddlewareName], + service: serviceName, + rule: rule, + priority: priority + }; } - const configDomain = config.getDomain(resource.domainId); - let tls = {}; if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { const domainParts = fullDomain.split("."); @@ -510,13 +429,101 @@ export async function getTraefikConfig( } } - const additionalMiddlewares = - config.getRawConfig().traefik.additional_middlewares || []; + const availableServers = (targets as TargetWithSite[]).filter( + (target: TargetWithSite) => { + if (!target.enabled) return false; - const routerMiddlewares = [ - badgerMiddlewareName, - ...additionalMiddlewares - ]; + const anySitesOnline = (targets as TargetWithSite[]).some( + (t: TargetWithSite) => t.site.online + ); + if (anySitesOnline && !target.site.online) return false; + + if (target.site.type === "local" || target.site.type === "wireguard") { + return target.ip && target.port && target.method; + } else if (target.site.type === "newt") { + return target.internalPort && target.method && target.site.subnet; + } + return false; + } + ); + + const hasHealthyServers = availableServers.length > 0; + + let showMaintenancePage = false; + if (resource.maintenanceModeEnabled) { + if (resource.maintenanceModeType === "forced") { + showMaintenancePage = true; + // logger.debug( + // `Resource ${resource.name} (${fullDomain}) is in FORCED maintenance mode` + // ); + } else if (resource.maintenanceModeType === "automatic") { + showMaintenancePage = !hasHealthyServers; + if (showMaintenancePage) { + logger.warn( + `Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)` + ); + } + } + } + + if (showMaintenancePage) { + const maintenanceServiceName = `${key}-maintenance-service`; + const maintenanceRouterName = `${key}-maintenance-router`; + const rewriteMiddlewareName = `${key}-maintenance-rewrite`; + + const entrypointHttp = config.getRawConfig().traefik.http_entrypoint; + const entrypointHttps = config.getRawConfig().traefik.https_entrypoint; + + const fullDomain = resource.fullDomain; + const domainParts = fullDomain.split("."); + const wildCard = resource.subdomain + ? `*.${domainParts.slice(1).join(".")}` + : fullDomain; + + const maintenancePort = config.getRawConfig().server.next_port; + const maintenanceHost = config.getRawConfig().server.internal_hostname; + + config_output.http.services[maintenanceServiceName] = { + loadBalancer: { + servers: [{ url: `http://${maintenanceHost}:${maintenancePort}` }], + passHostHeader: true + } + }; + + // middleware to rewrite path to /maintenance-screen + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + + config_output.http.middlewares[rewriteMiddlewareName] = { + replacePathRegex: { + regex: "^/(.*)", + replacement: "/maintenance-screen" + } + }; + + config_output.http.routers[maintenanceRouterName] = { + entryPoints: [resource.ssl ? entrypointHttps : entrypointHttp], + service: maintenanceServiceName, + middlewares: [rewriteMiddlewareName], + rule: rule, + priority: 2000, + ...(resource.ssl ? { tls } : {}) + }; + + // Router to allow Next.js assets to load without rewrite + config_output.http.routers[`${maintenanceRouterName}-assets`] = { + entryPoints: [resource.ssl ? entrypointHttps : entrypointHttp], + service: maintenanceServiceName, + rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`, + priority: 2001, + ...(resource.ssl ? { tls } : {}) + }; + + // logger.info(`Maintenance mode active for ${fullDomain}`); + + continue; + } // Handle path rewriting middleware if ( @@ -608,29 +615,6 @@ export async function getTraefikConfig( } } - let rule = `Host(\`${fullDomain}\`)`; - - // priority logic - let priority: number; - if (resource.priority && resource.priority != 100) { - priority = resource.priority; - } else { - priority = 100; - if (resource.path && resource.pathMatchType) { - priority += 10; - if (resource.pathMatchType === "exact") { - priority += 5; - } else if (resource.pathMatchType === "prefix") { - priority += 3; - } else if (resource.pathMatchType === "regex") { - priority += 2; - } - if (resource.path === "/") { - priority = 1; // lowest for catch-all - } - } - } - if (resource.path && resource.pathMatchType) { //priority += 1; // add path to rule based on match type @@ -661,18 +645,6 @@ export async function getTraefikConfig( ...(resource.ssl ? { tls } : {}) }; - if (resource.ssl) { - config_output.http.routers![routerName + "-redirect"] = { - entryPoints: [ - config.getRawConfig().traefik.http_entrypoint - ], - middlewares: [redirectHttpsMiddlewareName], - service: serviceName, - rule: rule, - priority: priority - }; - } - config_output.http.services![serviceName] = { loadBalancer: { servers: (() => { diff --git a/src/app/maintenance-screen/page.tsx b/src/app/maintenance-screen/page.tsx index a1e7bd22..7af97456 100644 --- a/src/app/maintenance-screen/page.tsx +++ b/src/app/maintenance-screen/page.tsx @@ -1,3 +1,4 @@ +import { Metadata } from "next"; import { headers } from "next/headers"; import { priv } from "@app/lib/api"; import { GetMaintenanceInfoResponse } from "@server/routers/resource/types"; @@ -10,9 +11,14 @@ import { } from "@app/components/ui/card"; import { Alert, AlertTitle, AlertDescription } from "@app/components/ui/alert"; import { Clock } from "lucide-react"; +import { AxiosResponse } from "axios"; export const dynamic = "force-dynamic"; +export const metadata: Metadata = { + title: "Maintenance" +}; + export default async function MaintenanceScreen() { const t = await getTranslations(); @@ -25,12 +31,12 @@ export default async function MaintenanceScreen() { const host = headersList.get("host") || ""; const hostname = host.split(":")[0]; - const res = await priv.get( + const res = await priv.get>( `/maintenance/info?fullDomain=${encodeURIComponent(hostname)}` ); if (res && res.status === 200) { - const maintenanceInfo = res.data; + const maintenanceInfo = res.data.data; title = maintenanceInfo?.maintenanceTitle || title; message = maintenanceInfo?.maintenanceMessage || message; estimatedTime = maintenanceInfo?.maintenanceEstimatedTime || null;