120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import { defineEventHandler, getMethod, readBody, getQuery, createError, getHeader } from 'h3'
|
|
import { getSubdomainFromRequest } from '~/server/utils/tenant'
|
|
import { getSessionToken } from '~/server/utils/session'
|
|
|
|
/**
|
|
* Catch-all API proxy that forwards requests to the NestJS backend
|
|
* Injects x-tenant-subdomain header and Authorization from HTTP-only cookie
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const config = useRuntimeConfig()
|
|
const method = getMethod(event)
|
|
const path = event.context.params?.path || ''
|
|
|
|
// Get subdomain and session token
|
|
const subdomain = getSubdomainFromRequest(event)
|
|
const token = getSessionToken(event)
|
|
|
|
const backendUrl = config.backendUrl || 'http://localhost:3000'
|
|
|
|
// Build the full URL with query parameters
|
|
const query = getQuery(event)
|
|
const queryString = new URLSearchParams(query as Record<string, string>).toString()
|
|
const fullUrl = `${backendUrl}/api/${path}${queryString ? `?${queryString}` : ''}`
|
|
|
|
console.log(`[BFF Proxy] ${method} ${fullUrl} (subdomain: ${subdomain}, hasToken: ${!!token})`)
|
|
|
|
// Build headers to forward
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': getHeader(event, 'content-type') || 'application/json',
|
|
}
|
|
|
|
// Add subdomain header for backend tenant resolution
|
|
if (subdomain) {
|
|
headers['x-tenant-subdomain'] = subdomain
|
|
}
|
|
|
|
// Add auth token from HTTP-only cookie
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
|
|
// Forward additional headers that might be needed
|
|
const acceptHeader = getHeader(event, 'accept')
|
|
if (acceptHeader) {
|
|
headers['Accept'] = acceptHeader
|
|
}
|
|
|
|
try {
|
|
// Prepare fetch options
|
|
const fetchOptions: RequestInit = {
|
|
method,
|
|
headers,
|
|
}
|
|
|
|
// Add body for methods that support it
|
|
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
const body = await readBody(event)
|
|
if (body) {
|
|
fetchOptions.body = JSON.stringify(body)
|
|
}
|
|
}
|
|
|
|
// Make request to backend
|
|
const response = await fetch(fullUrl, fetchOptions)
|
|
|
|
// Handle non-JSON responses (like file downloads)
|
|
const contentType = response.headers.get('content-type')
|
|
|
|
if (!response.ok) {
|
|
// Try to get error details
|
|
let errorMessage = `Backend error: ${response.status}`
|
|
let errorData = null
|
|
|
|
try {
|
|
errorData = await response.json()
|
|
errorMessage = errorData.message || errorMessage
|
|
} catch {
|
|
// Response wasn't JSON
|
|
try {
|
|
const text = await response.text()
|
|
console.error(`[BFF Proxy] Backend error (non-JSON): ${text}`)
|
|
} catch {}
|
|
}
|
|
|
|
console.error(`[BFF Proxy] Backend returned ${response.status}: ${errorMessage}`, errorData)
|
|
|
|
throw createError({
|
|
statusCode: response.status,
|
|
statusMessage: errorMessage,
|
|
data: errorData,
|
|
})
|
|
}
|
|
|
|
// Return empty response for 204 No Content
|
|
if (response.status === 204) {
|
|
return null
|
|
}
|
|
|
|
// Handle JSON responses
|
|
if (contentType?.includes('application/json')) {
|
|
return await response.json()
|
|
}
|
|
|
|
// Handle other content types (text, etc.)
|
|
return await response.text()
|
|
|
|
} catch (error: any) {
|
|
// Re-throw H3 errors
|
|
if (error.statusCode) {
|
|
throw error
|
|
}
|
|
|
|
console.error(`Proxy error for ${method} /api/${path}:`, error)
|
|
throw createError({
|
|
statusCode: 502,
|
|
statusMessage: 'Failed to connect to backend service',
|
|
})
|
|
}
|
|
})
|