This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { routing } from "./i18n/routing";
|
||||
|
||||
const intlMiddleware = createMiddleware(routing);
|
||||
const ADMIN_SESSION_COOKIE = "moh_admin_session";
|
||||
|
||||
async function isMaintenanceModeEnabled(request: NextRequest): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${request.nextUrl.origin}/api/maintenance`, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"x-middleware-cache": "bypass",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { enabled?: boolean };
|
||||
return data.enabled === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getLocaleFromPath(pathname: string): (typeof routing.locales)[number] {
|
||||
const firstSegment = pathname.split("/").filter(Boolean)[0];
|
||||
|
||||
if (routing.locales.includes(firstSegment as (typeof routing.locales)[number])) {
|
||||
return firstSegment as (typeof routing.locales)[number];
|
||||
}
|
||||
|
||||
return routing.defaultLocale;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function isSuperAdminSessionValid(request: NextRequest): Promise<boolean> {
|
||||
const token = request.cookies.get(ADMIN_SESSION_COOKIE)?.value;
|
||||
const secret = process.env.ADMIN_AUTH_SECRET;
|
||||
|
||||
if (!token || !secret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [value, signature] = parts;
|
||||
if (!value || !signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const signed = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key,
|
||||
new TextEncoder().encode(value),
|
||||
);
|
||||
const expected = Array.from(new Uint8Array(signed))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
return signature === expected;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
const locale = getLocaleFromPath(pathname);
|
||||
const isSuperAdmin = await isSuperAdminSessionValid(request);
|
||||
|
||||
const isRootRoute =
|
||||
pathname === `/${locale}/root` || pathname.startsWith(`/${locale}/root/`);
|
||||
const isComingSoonRoute =
|
||||
pathname === `/${locale}/coming-soon` ||
|
||||
pathname.startsWith(`/${locale}/coming-soon/`);
|
||||
|
||||
if (isRootRoute && isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) {
|
||||
return new NextResponse("Authentication required", {
|
||||
status: 401,
|
||||
headers: {
|
||||
"WWW-Authenticate": 'Basic realm="Root Area", charset="UTF-8"',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!isRootRoute && !isComingSoonRoute && !isSuperAdmin) {
|
||||
const maintenanceModeEnabled = await isMaintenanceModeEnabled(request);
|
||||
|
||||
if (maintenanceModeEnabled) {
|
||||
return NextResponse.redirect(new URL(`/${locale}/coming-soon`, request.url));
|
||||
}
|
||||
}
|
||||
|
||||
return intlMiddleware(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!api|trpc|_next|_vercel|.*\\..*).*)"],
|
||||
};
|
||||
Reference in New Issue
Block a user