fix(dashboard): proxy API requests through dashboard route
Build & Deploy / Build & Push Docker Image (push) Successful in 2m58s
Test / Type Check (all packages) (push) Successful in 54s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 40s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 39s
Test / API Integration Tests (push) Successful in 59s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m58s
Test / Type Check (all packages) (push) Successful in 54s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 40s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 39s
Test / API Integration Tests (push) Successful in 59s
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ path?: string[] }> | { path?: string[] }
|
||||
}
|
||||
|
||||
const HOP_BY_HOP_HEADERS = new Set([
|
||||
'connection',
|
||||
'content-encoding',
|
||||
'content-length',
|
||||
'host',
|
||||
'keep-alive',
|
||||
'proxy-authenticate',
|
||||
'proxy-authorization',
|
||||
'set-cookie',
|
||||
'te',
|
||||
'trailer',
|
||||
'transfer-encoding',
|
||||
'upgrade',
|
||||
])
|
||||
|
||||
function normalizeApiOrigin(value: string): string {
|
||||
return value.replace(/\/+$/, '').replace(/\/api(?:\/v1)?$/, '')
|
||||
}
|
||||
|
||||
function resolveApiOrigins(): string[] {
|
||||
return Array.from(new Set([
|
||||
process.env.API_INTERNAL_URL,
|
||||
process.env.API_URL,
|
||||
process.env.NEXT_PUBLIC_API_URL,
|
||||
'http://localhost:4000',
|
||||
]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map(normalizeApiOrigin)))
|
||||
}
|
||||
|
||||
function rewriteCookieForDashboard(cookie: string): string {
|
||||
return cookie
|
||||
.replace(/;\s*Domain=[^;]*/gi, '')
|
||||
.replace(/;\s*SameSite=None/gi, '; SameSite=Lax')
|
||||
}
|
||||
|
||||
function getSetCookieHeaders(headers: Headers): string[] {
|
||||
const withGetter = headers as Headers & { getSetCookie?: () => string[] }
|
||||
if (typeof withGetter.getSetCookie === 'function') return withGetter.getSetCookie()
|
||||
|
||||
const cookie = headers.get('set-cookie')
|
||||
return cookie ? [cookie] : []
|
||||
}
|
||||
|
||||
function copyRequestHeaders(request: Request): Headers {
|
||||
const headers = new Headers()
|
||||
request.headers.forEach((value, key) => {
|
||||
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) headers.set(key, value)
|
||||
})
|
||||
return headers
|
||||
}
|
||||
|
||||
function copyResponseHeaders(upstream: Response): Headers {
|
||||
const headers = new Headers()
|
||||
upstream.headers.forEach((value, key) => {
|
||||
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) headers.set(key, value)
|
||||
})
|
||||
return headers
|
||||
}
|
||||
|
||||
async function proxyDashboardApi(request: Request, context: RouteContext) {
|
||||
const params = await Promise.resolve(context.params)
|
||||
const path = (params.path ?? []).map(encodeURIComponent).join('/')
|
||||
const requestUrl = new URL(request.url)
|
||||
const method = request.method.toUpperCase()
|
||||
const hasBody = method !== 'GET' && method !== 'HEAD'
|
||||
const body = hasBody ? await request.arrayBuffer() : undefined
|
||||
const headers = copyRequestHeaders(request)
|
||||
let lastError: unknown = null
|
||||
let upstream: Response | null = null
|
||||
|
||||
for (const origin of resolveApiOrigins()) {
|
||||
const upstreamUrl = new URL(`/api/v1/${path}${requestUrl.search}`, origin)
|
||||
try {
|
||||
upstream = await fetch(upstreamUrl, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
cache: 'no-store',
|
||||
})
|
||||
break
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
if (!upstream) {
|
||||
const message = lastError instanceof Error ? lastError.message : 'API proxy request failed'
|
||||
return NextResponse.json({ error: 'api_proxy_unavailable', message }, { status: 502 })
|
||||
}
|
||||
|
||||
const response = new NextResponse(await upstream.arrayBuffer(), {
|
||||
status: upstream.status,
|
||||
statusText: upstream.statusText,
|
||||
headers: copyResponseHeaders(upstream),
|
||||
})
|
||||
|
||||
for (const cookie of getSetCookieHeaders(upstream.headers)) {
|
||||
response.headers.append('set-cookie', rewriteCookieForDashboard(cookie))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: Request, context: RouteContext) {
|
||||
return proxyDashboardApi(request, context)
|
||||
}
|
||||
|
||||
export async function POST(request: Request, context: RouteContext) {
|
||||
return proxyDashboardApi(request, context)
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, context: RouteContext) {
|
||||
return proxyDashboardApi(request, context)
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request, context: RouteContext) {
|
||||
return proxyDashboardApi(request, context)
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request, context: RouteContext) {
|
||||
return proxyDashboardApi(request, context)
|
||||
}
|
||||
Reference in New Issue
Block a user