120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
import { getToken } from "next-auth/jwt";
|
|
import { NextResponse, type NextRequest } from "next/server";
|
|
import {
|
|
getAuthMode,
|
|
getRequiredAuthSecret,
|
|
isAuthRoute,
|
|
isProtectedPath,
|
|
type AuthEnv,
|
|
} from "@/lib/auth-config";
|
|
|
|
export async function proxy(request: NextRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
if (!isProtectedPath(pathname)) return NextResponse.next();
|
|
|
|
const authMode = getAuthMode();
|
|
if (!authMode.enabled && !authMode.error) return NextResponse.next();
|
|
|
|
if (authMode.error) {
|
|
return authConfigErrorResponse(request, authMode.error);
|
|
}
|
|
|
|
const token = await getToken({
|
|
req: request,
|
|
secret: getRequiredAuthSecret(),
|
|
});
|
|
|
|
if (token) return NextResponse.next();
|
|
|
|
if (pathname.startsWith("/api/")) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const callbackUrl = buildPublicRequestUrl(request);
|
|
const signInUrl = new URL("/api/auth/signin/oidc", callbackUrl);
|
|
signInUrl.searchParams.set("callbackUrl", callbackUrl.href);
|
|
return NextResponse.redirect(signInUrl);
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/((?!_next/static|_next/image|favicon.ico|icon.svg).*)"],
|
|
};
|
|
|
|
function authConfigErrorResponse(request: NextRequest, error: string) {
|
|
if (request.nextUrl.pathname.startsWith("/api/") && !isAuthRoute(request.nextUrl.pathname)) {
|
|
return NextResponse.json({ error }, { status: 500 });
|
|
}
|
|
|
|
return new NextResponse(authErrorHtml(error), {
|
|
status: 500,
|
|
headers: {
|
|
"content-type": "text/html; charset=utf-8",
|
|
},
|
|
});
|
|
}
|
|
|
|
export function buildPublicRequestUrl(request: NextRequest, env: AuthEnv = process.env): URL {
|
|
const origin = getPublicOrigin(request, env);
|
|
return new URL(`${request.nextUrl.pathname}${request.nextUrl.search}`, origin);
|
|
}
|
|
|
|
function getPublicOrigin(request: NextRequest, env: AuthEnv): string {
|
|
const configuredOrigin = getConfiguredOrigin(env.NEXTAUTH_URL);
|
|
if (configuredOrigin) return configuredOrigin;
|
|
|
|
const forwardedHost = firstHeaderValue(request.headers.get("x-forwarded-host"));
|
|
const host = forwardedHost || firstHeaderValue(request.headers.get("host")) || request.nextUrl.host;
|
|
const forwardedProto = firstHeaderValue(request.headers.get("x-forwarded-proto"));
|
|
const proto = forwardedProto || request.nextUrl.protocol.replace(":", "") || "http";
|
|
|
|
return `${proto}://${host}`;
|
|
}
|
|
|
|
function getConfiguredOrigin(value: string | undefined): string | null {
|
|
if (!value?.trim()) return null;
|
|
|
|
try {
|
|
return new URL(value).origin;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function firstHeaderValue(value: string | null): string {
|
|
return value?.split(",")[0]?.trim() ?? "";
|
|
}
|
|
|
|
function authErrorHtml(error: string): string {
|
|
return `<!doctype html>
|
|
<html lang="zh">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Authentication Configuration Error</title>
|
|
<style>
|
|
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0f172a; color: #e2e8f0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
main { max-width: 640px; padding: 32px; }
|
|
h1 { margin: 0 0 12px; font-size: 22px; }
|
|
p { color: #94a3b8; line-height: 1.6; }
|
|
code { color: #f8fafc; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>OIDC 配置不完整</h1>
|
|
<p><code>${escapeHtml(error)}</code></p>
|
|
<p>请补全 OIDC 配置,或移除全部 OIDC 相关变量以保持开放访问。</p>
|
|
</main>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|