WIP - BFF
This commit is contained in:
111
frontend/server/api/[...path].ts
Normal file
111
frontend/server/api/[...path].ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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}` : ''}`
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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',
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user