Files
PolyWeather/frontend/middleware.ts
T
2569718930@qq.com 20c8395c0b 全局配置更新:OAuth 回调修复、支付安全加固、站点 URL 工具
- 新增 NEXT_PUBLIC_SITE_URL 支持及 site-url.ts 工具模块
- 修复 OAuth 回调域名:import.meta.env 统一读取站点 URL
- 支付 API 路由新增收款地址校验
- 后端支付服务更新
- middleware 清理
- 新增 paymentSecurity 测试
2026-05-24 18:33:47 +08:00

191 lines
5.1 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import {
createSupabaseMiddlewareClient,
hasSupabaseServerEnv,
} from "@/lib/supabase/server";
import { isLocalFullAccessHost } from "@/lib/local-dev-access";
const SESSION_COOKIE = "polyweather_entitlement";
function readEnvBool(name: string, fallback: boolean) {
const raw = process.env[name];
if (raw == null) return fallback;
return String(raw).trim().toLowerCase() === "true";
}
const SUPABASE_AUTH_ENABLED =
readEnvBool("POLYWEATHER_AUTH_ENABLED", false);
const SUPABASE_AUTH_REQUIRED = readEnvBool(
"POLYWEATHER_AUTH_REQUIRED",
SUPABASE_AUTH_ENABLED,
);
function isPublicPage(pathname: string) {
return (
pathname === "/" ||
pathname.startsWith("/docs") ||
pathname.startsWith("/subscription-help") ||
pathname === "/entitlement-required" ||
pathname.startsWith("/auth/login") ||
pathname.startsWith("/auth/callback")
);
}
function isPublicApi(pathname: string) {
return (
pathname === "/api/auth/me" ||
pathname === "/api/analytics/events" ||
pathname === "/api/cities" ||
pathname === "/api/scan/terminal" ||
pathname === "/api/vitals" ||
/^\/api\/city\/[^/]+$/i.test(pathname) ||
/^\/api\/city\/[^/]+\/summary$/i.test(pathname) ||
/^\/api\/city\/[^/]+\/detail$/i.test(pathname) ||
/^\/api\/city\/[^/]+\/market-scan$/i.test(pathname)
);
}
function shouldRefreshOptionalSupabaseSession(pathname: string) {
return (
pathname.startsWith("/account") ||
pathname.startsWith("/ops") ||
pathname.startsWith("/api/ops/") ||
pathname.startsWith("/api/payments/") ||
pathname === "/api/system/status"
);
}
function handleLegacyTokenGate(request: NextRequest) {
const requiredToken = process.env.POLYWEATHER_DASHBOARD_ACCESS_TOKEN?.trim();
if (!requiredToken) {
return NextResponse.next();
}
const { pathname, searchParams } = request.nextUrl;
if (isPublicPage(pathname) || isPublicApi(pathname)) {
return NextResponse.next();
}
const cookieToken = request.cookies.get(SESSION_COOKIE)?.value;
if (cookieToken && cookieToken === requiredToken) {
return NextResponse.next();
}
const queryToken = searchParams.get("access_token");
if (queryToken && queryToken === requiredToken) {
const cleanUrl = request.nextUrl.clone();
cleanUrl.searchParams.delete("access_token");
const response = NextResponse.redirect(cleanUrl);
response.cookies.set(SESSION_COOKIE, requiredToken, {
httpOnly: true,
sameSite: "lax",
secure: cleanUrl.protocol === "https:",
path: "/",
maxAge: 60 * 60 * 12,
});
return response;
}
if (pathname.startsWith("/api/")) {
return NextResponse.json(
{ error: "Unauthorized", detail: "Entitlement token required" },
{ status: 401 },
);
}
const deniedUrl = request.nextUrl.clone();
deniedUrl.pathname = "/entitlement-required";
deniedUrl.search = "";
deniedUrl.searchParams.set("next", pathname);
return NextResponse.redirect(deniedUrl);
}
async function handleSupabaseAuthGate(request: NextRequest) {
const { pathname } = request.nextUrl;
if (isPublicPage(pathname) || isPublicApi(pathname)) {
return NextResponse.next();
}
const response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createSupabaseMiddlewareClient(request, response);
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
return response;
}
if (pathname.startsWith("/api/")) {
return NextResponse.json(
{ error: "Unauthorized", detail: "Supabase session required" },
{ status: 401 },
);
}
const loginUrl = request.nextUrl.clone();
loginUrl.pathname = "/auth/login";
loginUrl.search = "";
loginUrl.searchParams.set("next", pathname);
return NextResponse.redirect(loginUrl);
}
async function handleSupabaseOptionalSession(request: NextRequest) {
const { pathname } = request.nextUrl;
if (
isPublicPage(pathname) ||
isPublicApi(pathname) ||
!shouldRefreshOptionalSupabaseSession(pathname)
) {
return NextResponse.next();
}
const response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createSupabaseMiddlewareClient(request, response);
await supabase.auth.getUser();
return response;
}
export async function middleware(request: NextRequest) {
const requestHost =
request.headers.get("x-forwarded-host") ||
request.headers.get("host") ||
request.nextUrl.host;
if (
isLocalFullAccessHost(requestHost) ||
isLocalFullAccessHost(request.nextUrl.hostname)
) {
return NextResponse.next();
}
if (SUPABASE_AUTH_ENABLED && hasSupabaseServerEnv()) {
if (SUPABASE_AUTH_REQUIRED) {
return handleSupabaseAuthGate(request);
}
return handleSupabaseOptionalSession(request);
}
return handleLegacyTokenGate(request);
}
export const config = {
matcher: [
"/account/:path*",
"/ops/:path*",
"/api/auth/:path*",
"/api/ops/:path*",
"/api/payments/:path*",
"/api/system/:path*",
"/api/city/:path*/detail:path*",
"/api/scan/terminal/ai:path*",
],
};