This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { unstable_noStore as noStore } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import { Navbar } from "@/components/navbar";
|
import { Navbar } from "@/components/navbar";
|
||||||
|
import { isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
import { getMaintenanceMode } from "@/lib/app-config";
|
import { getMaintenanceMode } from "@/lib/app-config";
|
||||||
import { resolveLocale } from "@/lib/site-data";
|
import { resolveLocale } from "@/lib/site-data";
|
||||||
|
|
||||||
@@ -13,11 +15,17 @@ type SiteLayoutProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
export default async function SiteLayout({ children, params: { locale } }: SiteLayoutProps) {
|
export default async function SiteLayout({ children, params: { locale } }: SiteLayoutProps) {
|
||||||
|
noStore();
|
||||||
|
|
||||||
const localeKey = resolveLocale(locale);
|
const localeKey = resolveLocale(locale);
|
||||||
const maintenanceEnabled = await getMaintenanceMode();
|
const maintenanceEnabled = await getMaintenanceMode();
|
||||||
|
const authenticated = isAdminAuthenticated();
|
||||||
|
|
||||||
if (maintenanceEnabled) {
|
if (maintenanceEnabled && !authenticated) {
|
||||||
redirect(`/${localeKey}/coming-soon`);
|
redirect(`/${localeKey}/coming-soon`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { unstable_noStore as noStore } from "next/cache";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
@@ -13,6 +14,9 @@ type LocaleLayoutProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return routing.locales.map((locale) => ({ locale }));
|
return routing.locales.map((locale) => ({ locale }));
|
||||||
}
|
}
|
||||||
@@ -21,6 +25,8 @@ export default async function LocaleLayout({
|
|||||||
children,
|
children,
|
||||||
params: { locale },
|
params: { locale },
|
||||||
}: LocaleLayoutProps) {
|
}: LocaleLayoutProps) {
|
||||||
|
noStore();
|
||||||
|
|
||||||
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -23,6 +23,7 @@ import {
|
|||||||
resetAdminFailedAttempts,
|
resetAdminFailedAttempts,
|
||||||
setAdminSessionCookie,
|
setAdminSessionCookie,
|
||||||
} from "@/lib/admin-auth";
|
} from "@/lib/admin-auth";
|
||||||
|
import { routing } from "@/i18n/routing";
|
||||||
import { getMaintenanceMode, setMaintenanceMode } from "@/lib/app-config";
|
import { getMaintenanceMode, setMaintenanceMode } from "@/lib/app-config";
|
||||||
import { resolveLocale } from "@/lib/site-data";
|
import { resolveLocale } from "@/lib/site-data";
|
||||||
|
|
||||||
@@ -86,8 +87,12 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
const nextValue = formData.get("enabled") === "true";
|
const nextValue = formData.get("enabled") === "true";
|
||||||
|
|
||||||
await setMaintenanceMode(nextValue);
|
await setMaintenanceMode(nextValue);
|
||||||
|
revalidatePath("/", "layout");
|
||||||
revalidatePath("/root");
|
revalidatePath("/root");
|
||||||
revalidatePath(`/${localeKey}/coming-soon`);
|
for (const appLocale of routing.locales) {
|
||||||
|
revalidatePath(`/${appLocale}`, "layout");
|
||||||
|
revalidatePath(`/${appLocale}/coming-soon`);
|
||||||
|
}
|
||||||
redirect("/root");
|
redirect("/root");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+118
-1
@@ -5,6 +5,8 @@ import type { NextRequest } from "next/server";
|
|||||||
import { routing } from "./i18n/routing";
|
import { routing } from "./i18n/routing";
|
||||||
|
|
||||||
const intlMiddleware = createMiddleware(routing);
|
const intlMiddleware = createMiddleware(routing);
|
||||||
|
const ADMIN_SESSION_COOKIE = "moh_admin_session";
|
||||||
|
const ADMIN_SESSION_VALUE = "superadmin";
|
||||||
|
|
||||||
function isRootBasicAuthConfigured(): boolean {
|
function isRootBasicAuthConfigured(): boolean {
|
||||||
return Boolean(process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS);
|
return Boolean(process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS);
|
||||||
@@ -39,10 +41,102 @@ function isRootBasicAuthValid(request: NextRequest): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<string | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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) {
|
export default async function middleware(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
const isRootBaseRoute = pathname === "/root" || pathname.startsWith("/root/");
|
const isRootBaseRoute = pathname === "/root" || pathname.startsWith("/root/");
|
||||||
|
|
||||||
const isRootRoute = isRootBaseRoute;
|
const isRootRoute = isRootBaseRoute;
|
||||||
|
|
||||||
if (isRootRoute && isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) {
|
if (isRootRoute && isRootBasicAuthConfigured() && !isRootBasicAuthValid(request)) {
|
||||||
@@ -54,6 +148,29 @@ export default async function middleware(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = `/${targetLocale}/coming-soon`;
|
||||||
|
redirectUrl.search = "";
|
||||||
|
return NextResponse.redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isRootBaseRoute) {
|
if (isRootBaseRoute) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user