Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions app/api/payments/checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,19 @@ export async function POST(req: NextRequest) {
// global NEXT_PUBLIC_APP_URL — this route is hit on the school's subdomain,
// and Solana Pay tx-request links must round-trip back to that same host
// (a QR built with the wrong tenant's origin fails for every other tenant).
const forwardedHost = req.headers.get('x-forwarded-host')
const appUrl =
process.env.NODE_ENV === 'development'
? req.nextUrl.origin
: forwardedHost
? `https://${forwardedHost}`
: req.nextUrl.origin
//
// req.nextUrl.origin does NOT reflect the incoming Host header in dev — it
// resolves to the Next.js dev server's own bind address (localhost:PORT)
// regardless of which tenant subdomain the request actually came in on.
// Confirmed live via #479: a PayPal checkout on code-academy.lvh.me built
// its return_url from req.nextUrl.origin, sending the buyer back to
// localhost:3000/checkout/success after approval — a different origin than
// their session cookie, which bounced them to /auth/login despite the
// purchase having succeeded. Trust the Host header instead, same pattern
// as app/api/stripe/connect/route.ts.
const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? req.nextUrl.host
const proto = req.headers.get('x-forwarded-proto') ?? req.nextUrl.protocol.replace(':', '')
const appUrl = `${proto}://${host}`

try {
const provider = getPaymentProvider(providerSlug as PaymentProvider)
Expand Down
18 changes: 11 additions & 7 deletions app/api/payments/paypal/capture/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ function getSupabaseAdmin() {
return createClient(url, serviceKey)
}

/** Resolve this request's own origin (tenant subdomain aware, like checkout). */
/**
* Resolve this request's own origin (tenant subdomain aware, like checkout).
*
* req.nextUrl.origin does NOT reflect the incoming Host header in dev — it
* resolves to the Next.js dev server's own bind address regardless of which
* tenant subdomain the request came in on (confirmed live via #479). Trust
* the Host header instead, same pattern as app/api/stripe/connect/route.ts.
*/
function requestOrigin(req: NextRequest): string {
const forwardedHost = req.headers.get('x-forwarded-host')
return process.env.NODE_ENV === 'development'
? req.nextUrl.origin
: forwardedHost
? `https://${forwardedHost}`
: req.nextUrl.origin
const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? req.nextUrl.host
const proto = req.headers.get('x-forwarded-proto') ?? req.nextUrl.protocol.replace(':', '')
return `${proto}://${host}`
}

/** Follow `next` only when it points back at our own origin. */
Expand Down
Loading