diff --git a/src/lib/runtime/storage.ts b/src/lib/runtime/storage.ts index 8751a29..a52cedf 100644 --- a/src/lib/runtime/storage.ts +++ b/src/lib/runtime/storage.ts @@ -1,4 +1,5 @@ -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; +// All API calls go through the server route gateway proxy +const API_GATEWAY = '/api/gateway'; export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding'; export type RuntimeRecordStatus = 'draft' | 'published'; @@ -28,12 +29,12 @@ export async function saveRuntimeConfig(type: RuntimeRecordType, key: string, pa try { // 1. Ensure the Role exists first. We lookup by key. // If it doesn't exist, we create it generically. - let roleRes = await fetch(`${API_URL}/api/admin/roles/${normalizedKey}`); + let roleRes = await fetch(`${API_GATEWAY}/api/admin/roles/${normalizedKey}`); let role: RoleRecord; if (!roleRes.ok && roleRes.status === 404) { // Create it - const createRes = await fetch(`${API_URL}/api/admin/roles`, { + const createRes = await fetch(`${API_GATEWAY}/api/admin/roles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -68,7 +69,7 @@ export async function saveRuntimeConfig(type: RuntimeRecordType, key: string, pa bodyPayload.config_json = payload; } - const saveRes = await fetch(`${API_URL}${endpointMap[type]}`, { + const saveRes = await fetch(`${API_GATEWAY}${endpointMap[type]}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyPayload) @@ -93,7 +94,7 @@ export async function listRuntimeConfigs(type: RuntimeRecordType): Promise(type: RuntimeRecordType, roleId: strin }; try { - const res = await fetch(`${API_URL}${endpointMap[type]}`); + const res = await fetch(`${API_GATEWAY}${endpointMap[type]}`); if (!res.ok) return null; const data = await res.json(); diff --git a/src/lib/server/gateway.ts b/src/lib/server/gateway.ts new file mode 100644 index 0000000..e38ad38 --- /dev/null +++ b/src/lib/server/gateway.ts @@ -0,0 +1,34 @@ +// Server-side helper: all backend calls go through the Rust gateway +const GATEWAY_URL = (process.env.GATEWAY_URL || 'http://localhost:8000').replace(/\/+$/, ''); + +export function gatewayUrl(path: string): string { + const normalized = path.startsWith('/') ? path : `/${path}`; + return `${GATEWAY_URL}${normalized}`; +} + +/** Forward the Authorization header from an incoming browser request to the gateway */ +export function forwardAuth(request: Request): Record { + const auth = request.headers.get('Authorization'); + return auth ? { Authorization: auth } : {}; +} + +/** Forward the Cookie header from an incoming browser request to the gateway */ +export function forwardCookies(request: Request): Record { + const cookie = request.headers.get('cookie'); + return cookie ? { cookie } : {}; +} + +/** + * Merge auth + cookie headers from the incoming request with any extra headers provided. + */ +export function withAuthHeaders( + request: Request, + extra: Record = {}, +): Record { + return { + 'Content-Type': 'application/json', + ...forwardAuth(request), + ...forwardCookies(request), + ...extra, + }; +} diff --git a/src/routes/api/gateway/[...path].ts b/src/routes/api/gateway/[...path].ts new file mode 100644 index 0000000..81b60ac --- /dev/null +++ b/src/routes/api/gateway/[...path].ts @@ -0,0 +1,76 @@ +import { gatewayUrl, withAuthHeaders } from '~/lib/server/gateway'; + +/** + * Generic gateway proxy endpoint for admin panel + * Forwards all requests to the Rust backend gateway with proper auth headers + */ +export async function GET({ request, params }: { request: Request; params: any }) { + return proxyRequest('GET', request, params); +} + +export async function POST({ request, params }: { request: Request; params: any }) { + return proxyRequest('POST', request, params); +} + +export async function PUT({ request, params }: { request: Request; params: any }) { + return proxyRequest('PUT', request, params); +} + +export async function DELETE({ request, params }: { request: Request; params: any }) { + return proxyRequest('DELETE', request, params); +} + +export async function PATCH({ request, params }: { request: Request; params: any }) { + return proxyRequest('PATCH', request, params); +} + +async function proxyRequest(method: string, request: Request, params: any) { + try { + // Handle different param structures + let pathArray = params.path; + if (!Array.isArray(pathArray)) { + pathArray = [pathArray]; + } + + const path = `/${pathArray.join('/')}`; + const url = new URL(request.url); + const queryString = url.search ? url.search : ''; + + let body: string | undefined; + if (['POST', 'PUT', 'PATCH'].includes(method)) { + body = await request.text(); + } + + const upstreamUrl = gatewayUrl(path + queryString); + const upstreamRequest = new Request(upstreamUrl, { + method, + headers: withAuthHeaders(request, { + 'Content-Type': request.headers.get('Content-Type') || 'application/json', + }), + body, + cache: 'no-store', + }); + + const response = await fetch(upstreamRequest); + const responseHeaders = new Headers(); + response.headers.forEach((value, key) => { + if (!['server', 'transfer-encoding', 'connection'].includes(key.toLowerCase())) { + responseHeaders.set(key, value); + } + }); + responseHeaders.set('Content-Type', 'application/json'); + + const responseBody = await response.text(); + + return new Response(responseBody, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error: any) { + return new Response( + JSON.stringify({ success: false, error: error?.message || 'Internal Server Error' }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +}