import createMiddleware from "next-intl/middleware"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { routing } from "./i18n/routing"; import { getLocalizedPath } from "./lib/locale"; const intlMiddleware = createMiddleware(routing); const ADMIN_SESSION_COOKIE = "moh_admin_session"; const ADMIN_SESSION_VALUE = "superadmin"; function isRootBasicAuthConfigured(): boolean { return Boolean(process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS); } function isRootBasicAuthValid(request: NextRequest): boolean { if (!isRootBasicAuthConfigured()) { return false; } const header = request.headers.get("authorization"); if (!header || !header.startsWith("Basic ")) { return false; } try { const decoded = atob(header.slice(6)); const index = decoded.indexOf(":"); if (index === -1) { return false; } const user = decoded.slice(0, index); const pass = decoded.slice(index + 1); return ( user === process.env.ROOT_BASIC_AUTH_USER && pass === process.env.ROOT_BASIC_AUTH_PASS ); } catch { return false; } } function getLocaleFromPathname(pathname: string): string | null { for (const locale of routing.locales) { if (pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)) { return locale; } } return null; } function timingSafeEqualString(a: string, b: string): boolean { if (a.length !== b.length) { return false; } let mismatch = 0; for (let index = 0; index < a.length; index += 1) { mismatch |= a.charCodeAt(index) ^ b.charCodeAt(index); } return mismatch === 0; } function toHex(buffer: ArrayBuffer): string { return Array.from(new Uint8Array(buffer)) .map((value) => value.toString(16).padStart(2, "0")) .join(""); } async function signAdminValue(value: string): Promise { const secret = process.env.ADMIN_AUTH_SECRET ?? ""; if (!secret) { return null; } const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(value)); return toHex(signature); } async function isAdminSessionValid(request: NextRequest): Promise { const token = request.cookies.get(ADMIN_SESSION_COOKIE)?.value; if (!token) { return false; } const parts = token.split("."); if (parts.length !== 2) { return false; } const [value, signature] = parts; if (value !== ADMIN_SESSION_VALUE) { return false; } const expected = await signAdminValue(value); if (!expected) { return false; } return timingSafeEqualString(signature, expected); } async function getMaintenanceModeFromApi(request: NextRequest): Promise { try { const response = await fetch(new URL("/api/maintenance", request.nextUrl.origin), { cache: "no-store", headers: { "x-middleware-check": "1", }, }); if (!response.ok) { return false; } const data = (await response.json()) as { enabled?: boolean }; return data.enabled === true; } catch { return false; } } export default async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const isRootBaseRoute = pathname === "/root" || pathname.startsWith("/root/"); const isRootRoute = isRootBaseRoute; if (pathname === "/de" || pathname.startsWith("/de/")) { const redirectUrl = request.nextUrl.clone(); const nextPath = pathname.slice(3) || "/"; redirectUrl.pathname = nextPath; return NextResponse.redirect(redirectUrl, 308); } if (isRootRoute && isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) { return new NextResponse("Authentication required", { status: 401, headers: { "WWW-Authenticate": 'Basic realm="Root Area", charset="UTF-8"', }, }); } const locale = getLocaleFromPathname(pathname); const isComingSoonRoute = locale !== null && (pathname === `/${locale}/coming-soon` || pathname.startsWith(`/${locale}/coming-soon/`)); const isLocalizedSiteRoute = locale !== null && !isComingSoonRoute; const isTopLevelRootRoute = pathname === "/"; if (!isRootBaseRoute && (isLocalizedSiteRoute || isTopLevelRootRoute)) { const maintenanceEnabled = await getMaintenanceModeFromApi(request); if (maintenanceEnabled) { const isAdminAuthenticated = await isAdminSessionValid(request); if (!isAdminAuthenticated) { const targetLocale = locale ?? routing.defaultLocale; const redirectUrl = request.nextUrl.clone(); redirectUrl.pathname = getLocalizedPath(targetLocale, "/coming-soon"); redirectUrl.search = ""; return NextResponse.redirect(redirectUrl); } } } if (isRootBaseRoute) { return NextResponse.next(); } return intlMiddleware(request); } export const config = { matcher: ["/((?!api|trpc|_next|_vercel|.*\\..*).*)"], };