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).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 = { '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', }) } })