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 {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -25,7 +25,7 @@ export async function POST(
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -25,7 +25,7 @@ export async function POST(
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -25,7 +25,7 @@ export async function POST(
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
+2 -2
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -36,7 +36,7 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
@@ -18,7 +18,7 @@ export async function POST(req: NextRequest) {
try {
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const res = await fetch(`${API_BASE}/api/payments/reconcile-latest`, {
method: "POST",
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -21,7 +21,7 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
+2 -2
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -45,7 +45,7 @@ export async function DELETE(req: NextRequest) {
}
try {
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
requireBackendAuthUser,
requireBackendPaymentAuth,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
@@ -21,7 +21,7 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const authError = requireBackendAuthUser(auth);
const authError = requireBackendPaymentAuth(auth);
if (authError) return authError;
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
@@ -100,6 +100,11 @@ export function runTests() {
backendAuthSource.includes("requireBackendAuthUser"),
"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");
assert(
@@ -205,8 +210,9 @@ export function runTests() {
for (const route of paymentRoutes) {
const routeSource = fs.readFileSync(path.join(projectRoot, route), "utf8");
assert(
routeSource.includes("requireBackendAuthUser"),
`${route} must reject payment mutations without a real Supabase user`,
routeSource.includes("requireBackendPaymentAuth") &&
!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;
authUserId?: string | null;
authEmail?: string | null;
hasBearerAuth?: boolean;
};
type HeaderBuildOptions = {
@@ -53,13 +54,25 @@ export async function buildBackendRequestHeaders(
const incomingAuth = extractBearerToken(request.headers.get("authorization"));
if (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;
if (hasSupabaseServerEnv() && includeSupabaseIdentity) {
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 });
@@ -81,10 +94,22 @@ export async function buildBackendRequestHeaders(
if (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(
@@ -110,3 +135,8 @@ export function requireBackendAuthUser(auth: BackendHeaderBuildResult) {
auth.response,
);
}
export function requireBackendPaymentAuth(auth: BackendHeaderBuildResult) {
if (auth.authUserId || auth.hasBearerAuth) return null;
return requireBackendAuthUser(auth);
}