feat: implement payment proxy routes and add security validation tests for wallet and intent operations

This commit is contained in:
2569718930@qq.com
2026-05-30 15:41:15 +08:00
parent 853a652e9b
commit c0b1edaed0
10 changed files with 58 additions and 22 deletions
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -25,7 +25,7 @@ export async function POST(
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -25,7 +25,7 @@ export async function POST(
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -25,7 +25,7 @@ export async function POST(
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
+2 -2
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -36,7 +36,7 @@ export async function POST(req: NextRequest) {
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy"; import { buildProxyExceptionResponse } from "@/lib/api-proxy";
@@ -18,7 +18,7 @@ export async function POST(req: NextRequest) {
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const res = await fetch(`${API_BASE}/api/payments/reconcile-latest`, { const res = await fetch(`${API_BASE}/api/payments/reconcile-latest`, {
method: "POST", method: "POST",
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -21,7 +21,7 @@ export async function POST(req: NextRequest) {
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
+2 -2
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -45,7 +45,7 @@ export async function DELETE(req: NextRequest) {
} }
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { import {
applyAuthResponseCookies, applyAuthResponseCookies,
buildBackendRequestHeaders, buildBackendRequestHeaders,
requireBackendAuthUser, requireBackendPaymentAuth,
} from "@/lib/backend-auth"; } from "@/lib/backend-auth";
import { import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
@@ -21,7 +21,7 @@ export async function POST(req: NextRequest) {
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth); const authError = requireBackendPaymentAuth(auth);
if (authError) return authError; if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers); const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json"); proxiedHeaders.set("Content-Type", "application/json");
@@ -100,6 +100,11 @@ export function runTests() {
backendAuthSource.includes("requireBackendAuthUser"), backendAuthSource.includes("requireBackendAuthUser"),
"backend auth helper must expose a real-user requirement for payment mutations", "backend auth helper must expose a real-user requirement for payment mutations",
); );
assert(
backendAuthSource.includes("requireBackendPaymentAuth") &&
backendAuthSource.includes("hasBearerAuth"),
"backend auth helper must allow bearer-backed payment mutations to reach the backend verifier",
);
const middlewareSource = fs.readFileSync(middlewarePath, "utf8"); const middlewareSource = fs.readFileSync(middlewarePath, "utf8");
assert( assert(
@@ -205,8 +210,9 @@ export function runTests() {
for (const route of paymentRoutes) { for (const route of paymentRoutes) {
const routeSource = fs.readFileSync(path.join(projectRoot, route), "utf8"); const routeSource = fs.readFileSync(path.join(projectRoot, route), "utf8");
assert( assert(
routeSource.includes("requireBackendAuthUser"), routeSource.includes("requireBackendPaymentAuth") &&
`${route} must reject payment mutations without a real Supabase user`, !routeSource.includes("requireBackendAuthUser(auth)"),
`${route} must allow bearer-backed payment mutations while still rejecting requests with no auth context`,
); );
} }
} }
+34 -4
View File
@@ -11,6 +11,7 @@ export type BackendHeaderBuildResult = {
response: NextResponse | null; response: NextResponse | null;
authUserId?: string | null; authUserId?: string | null;
authEmail?: string | null; authEmail?: string | null;
hasBearerAuth?: boolean;
}; };
type HeaderBuildOptions = { type HeaderBuildOptions = {
@@ -53,13 +54,25 @@ export async function buildBackendRequestHeaders(
const incomingAuth = extractBearerToken(request.headers.get("authorization")); const incomingAuth = extractBearerToken(request.headers.get("authorization"));
if (incomingAuth) { if (incomingAuth) {
headers.set("Authorization", `Bearer ${incomingAuth}`); headers.set("Authorization", `Bearer ${incomingAuth}`);
return { headers, response: null, authUserId: null, authEmail: null }; return {
headers,
response: null,
authUserId: null,
authEmail: null,
hasBearerAuth: true,
};
} }
const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false; const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false;
if (hasSupabaseServerEnv() && includeSupabaseIdentity) { if (hasSupabaseServerEnv() && includeSupabaseIdentity) {
if (!hasSupabaseSessionCookie(request)) { if (!hasSupabaseSessionCookie(request)) {
return { headers, response: null, authUserId: null, authEmail: null }; return {
headers,
response: null,
authUserId: null,
authEmail: null,
hasBearerAuth: false,
};
} }
const passthroughResponse = new NextResponse(null, { status: 200 }); const passthroughResponse = new NextResponse(null, { status: 200 });
@@ -81,10 +94,22 @@ export async function buildBackendRequestHeaders(
if (forwardedEmail) { if (forwardedEmail) {
headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, forwardedEmail); headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, forwardedEmail);
} }
return { headers, response: passthroughResponse, authUserId: forwardedUserId || null, authEmail: forwardedEmail || null }; return {
headers,
response: passthroughResponse,
authUserId: forwardedUserId || null,
authEmail: forwardedEmail || null,
hasBearerAuth: Boolean(accessToken),
};
} }
return { headers, response: null, authUserId: null, authEmail: null }; return {
headers,
response: null,
authUserId: null,
authEmail: null,
hasBearerAuth: false,
};
} }
export function applyAuthResponseCookies( export function applyAuthResponseCookies(
@@ -110,3 +135,8 @@ export function requireBackendAuthUser(auth: BackendHeaderBuildResult) {
auth.response, auth.response,
); );
} }
export function requireBackendPaymentAuth(auth: BackendHeaderBuildResult) {
if (auth.authUserId || auth.hasBearerAuth) return null;
return requireBackendAuthUser(auth);
}