Files
wehub-resource-sync d68f003000
CI / Change detection (push) Has been cancelled
CI / Version drift (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Workflow RLM cache (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / npm wrapper smoke (push) Has been cancelled
CI / Mobile runtime smoke (push) Has been cancelled
CI / Workflow lint (push) Has been cancelled
CI / Documentation (push) Has been cancelled
Web Frontend / Lint & Type Check (push) Failing after 1s
Auto-close harvested PRs / close (push) Failing after 1s
cargo-deny / cargo-deny (bans licenses sources) (push) Failing after 1s
Security audit / cargo-audit (push) Failing after 1s
Sync to CNB / sync (push) Failing after 1s
Web Frontend / Deploy to Cloudflare (push) Waiting to run
cargo-deny / cargo-deny (advisories) (push) Failing after 3s
chore: import upstream snapshot with attribution
2026-07-13 12:08:23 +08:00

72 lines
2.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { locales, defaultLocale } from "@/lib/i18n/config";
const COOKIE = "NEXT_LOCALE";
const SECURITY_HEADERS: Record<string, string> = {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), interest-cohort=()",
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
};
function applySecurityHeaders(res: NextResponse): NextResponse {
for (const [k, v] of Object.entries(SECURITY_HEADERS)) res.headers.set(k, v);
return res;
}
function detectLocale(req: NextRequest): string {
// 1. Cookie
const cookie = req.cookies.get(COOKIE)?.value;
if (cookie && (locales as readonly string[]).includes(cookie)) return cookie;
// 2. Accept-Language header — match against all shipped locales
const accept = req.headers.get("accept-language") ?? "";
if (accept) {
const preferred = accept.split(",").map((s) => s.split(";")[0].trim().split("-")[0].toLowerCase());
for (const lang of preferred) {
if ((locales as readonly string[]).includes(lang)) return lang;
}
}
return defaultLocale;
}
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// Skip API routes, static files, _next, and the dot-less metadata route
// for the shared OG image (but still apply security headers).
if (
pathname.startsWith("/api/") ||
pathname.startsWith("/_next/") ||
pathname === "/opengraph-image" ||
pathname.includes(".")
) {
return applySecurityHeaders(NextResponse.next());
}
// Check if locale is already in path
const seg = pathname.split("/")[1];
if (locales.includes(seg as typeof locales[number])) {
const res = NextResponse.next();
res.cookies.set(COOKIE, seg, { path: "/", maxAge: 60 * 60 * 24 * 365 });
return applySecurityHeaders(res);
}
// Redirect bare paths to detected locale
const locale = detectLocale(req);
const url = req.nextUrl.clone();
url.pathname = `/${locale}${pathname}`;
const res = NextResponse.redirect(url);
res.cookies.set(COOKIE, locale, { path: "/", maxAge: 60 * 60 * 24 * 365 });
return applySecurityHeaders(res);
}
export const config = {
// Match everything so security headers apply globally; the function
// bypasses redirect/locale logic for /_next, /api, and dotted paths.
matcher: ["/:path*"],
};