From 50c3f5cce9819ae92caae842d897e7107c1bd459 Mon Sep 17 00:00:00 2001 From: MOH Date: Sun, 15 Mar 2026 04:33:07 +0100 Subject: [PATCH] Fix maintenance redirects and admin host detection --- app/api/site/default-locale/route.ts | 8 +++- lib/admin-routing.ts | 20 ++++++--- middleware.ts | 64 +++++++++++++++++++++++----- tests/admin-routing.test.ts | 17 ++++++++ 4 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 tests/admin-routing.test.ts diff --git a/app/api/site/default-locale/route.ts b/app/api/site/default-locale/route.ts index ed7e4fe..a00c378 100644 --- a/app/api/site/default-locale/route.ts +++ b/app/api/site/default-locale/route.ts @@ -1,15 +1,19 @@ import { NextResponse } from "next/server"; -import { getSiteSettings } from "@/lib/app-config"; +import { getMaintenanceMode, getSiteSettings } from "@/lib/app-config"; export const dynamic = "force-dynamic"; export async function GET() { - const settings = await getSiteSettings(); + const [settings, maintenanceEnabled] = await Promise.all([ + getSiteSettings(), + getMaintenanceMode(), + ]); return NextResponse.json( { defaultLocale: settings.defaultLocale, + maintenanceEnabled, }, { headers: { diff --git a/lib/admin-routing.ts b/lib/admin-routing.ts index 1aab492..7a3aaf5 100644 --- a/lib/admin-routing.ts +++ b/lib/admin-routing.ts @@ -43,12 +43,20 @@ export function getSiteHost(): string { return parseHostname(process.env.NEXT_PUBLIC_SITE_URL ?? DEFAULT_SITE_URL) ?? "localhost"; } -export function getRequestHostname(hostHeader?: string | null): string { - return (hostHeader ?? "") - .split(",")[0] - ?.trim() - .toLowerCase() - .replace(/:\d+$/, "") ?? ""; +export function getRequestHostname(...hostHeaders: Array): string { + for (const hostHeader of hostHeaders) { + const normalizedHostname = (hostHeader ?? "") + .split(",")[0] + ?.trim() + .toLowerCase() + .replace(/:\d+$/, "") ?? ""; + + if (normalizedHostname) { + return normalizedHostname; + } + } + + return ""; } export function isAdminHost(hostname: string): boolean { diff --git a/middleware.ts b/middleware.ts index eb3e25c..4d78d08 100644 --- a/middleware.ts +++ b/middleware.ts @@ -14,11 +14,31 @@ import { isLegacyAdminPath, toInternalAdminPath, } from "./lib/admin-routing"; -import { getLocalizedPathWithDefault } from "./lib/locale"; +import { getLocalizedPathWithDefault, stripLocalePrefix } from "./lib/locale"; const intlMiddleware = createMiddleware(routing); +const ADMIN_SESSION_COOKIE = "moh_admin_session"; -async function getConfiguredDefaultLocale(request: NextRequest) { +type SiteRuntimeState = { + defaultLocale: (typeof routing.locales)[number]; + maintenanceEnabled: boolean; +}; + +function isSupportedLocale(locale: string | undefined): locale is (typeof routing.locales)[number] { + return locale === "ar" || locale === "en" || locale === "de"; +} + +function getPathLocale(pathname: string, fallbackLocale: (typeof routing.locales)[number]) { + const locale = pathname.split("/")[1]; + + return isSupportedLocale(locale) ? locale : fallbackLocale; +} + +function isComingSoonPath(pathname: string) { + return stripLocalePrefix(pathname) === "/coming-soon"; +} + +async function getSiteRuntimeState(request: NextRequest): Promise { try { const response = await fetch(new URL("/api/site/default-locale", request.url), { headers: { @@ -28,16 +48,26 @@ async function getConfiguredDefaultLocale(request: NextRequest) { }); if (!response.ok) { - return routing.defaultLocale; + return { + defaultLocale: routing.defaultLocale, + maintenanceEnabled: false, + }; } - const data = await response.json() as { defaultLocale?: string }; + const data = await response.json() as { + defaultLocale?: string; + maintenanceEnabled?: boolean; + }; - return data.defaultLocale === "ar" || data.defaultLocale === "en" || data.defaultLocale === "de" - ? data.defaultLocale - : routing.defaultLocale; + return { + defaultLocale: isSupportedLocale(data.defaultLocale) ? data.defaultLocale : routing.defaultLocale, + maintenanceEnabled: data.maintenanceEnabled === true, + }; } catch { - return routing.defaultLocale; + return { + defaultLocale: routing.defaultLocale, + maintenanceEnabled: false, + }; } } @@ -88,7 +118,9 @@ export default async function middleware(request: NextRequest) { const isDevelopmentAdminRequest = process.env.NODE_ENV !== "production" && isDevelopmentAdminPath(pathname); const hostname = getRequestHostname( - request.headers.get("host") ?? request.headers.get("x-forwarded-host") ?? request.nextUrl.hostname, + request.headers.get("x-forwarded-host"), + request.headers.get("host"), + request.nextUrl.hostname, ); const isAdminRequest = isDevelopmentAdminRequest || isAdminHost(hostname); const hasDedicatedAdminHostname = hasDedicatedAdminHost(); @@ -139,7 +171,19 @@ export default async function middleware(request: NextRequest) { return response; } - const configuredDefaultLocale = await getConfiguredDefaultLocale(request); + const siteRuntimeState = await getSiteRuntimeState(request); + const configuredDefaultLocale = siteRuntimeState.defaultLocale; + + if ( + siteRuntimeState.maintenanceEnabled && + !request.cookies.has(ADMIN_SESSION_COOKIE) && + !isComingSoonPath(pathname) + ) { + const locale = getPathLocale(pathname, configuredDefaultLocale); + const redirectUrl = request.nextUrl.clone(); + redirectUrl.pathname = getLocalizedPathWithDefault(locale, "/coming-soon", configuredDefaultLocale); + return NextResponse.redirect(redirectUrl, 307); + } if ( configuredDefaultLocale !== routing.defaultLocale && diff --git a/tests/admin-routing.test.ts b/tests/admin-routing.test.ts new file mode 100644 index 0000000..95bd91b --- /dev/null +++ b/tests/admin-routing.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; + +import { getRequestHostname } from "../lib/admin-routing"; + +describe("admin routing helpers", () => { + it("prefers the first non-empty forwarded hostname", () => { + expect(getRequestHostname("root.mohfarawati.de", "internal-service")).toBe("root.mohfarawati.de"); + }); + + it("normalizes ports and comma-separated proxy values", () => { + expect(getRequestHostname(undefined, "root.mohfarawati.de:443, proxy")).toBe("root.mohfarawati.de"); + }); + + it("falls back to an empty string when no hostname exists", () => { + expect(getRequestHostname(undefined, null, "")).toBe(""); + }); +});