From cd223a709c5b5c0473093c953960acec789dd50b Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Tue, 14 Jul 2026 19:09:05 +0000 Subject: [PATCH] Deploy --- src/app/api/medusa/[...path]/route.ts | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/app/api/medusa/[...path]/route.ts diff --git a/src/app/api/medusa/[...path]/route.ts b/src/app/api/medusa/[...path]/route.ts new file mode 100644 index 0000000..58b9d3b --- /dev/null +++ b/src/app/api/medusa/[...path]/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import http from 'node:http' +import https from 'node:https' + +// Service-type routing: NEXT_PUBLIC_MEDUSA_SERVICE_TYPE (from .env.local) selects the correct +// internal Medusa container. Falls back to ecommerce. +const _st = process.env.NEXT_PUBLIC_MEDUSA_SERVICE_TYPE || 'ecommerce' +const _urlMap: Record = { + ecommerce: process.env.VULA_ECOMMERCE_BACKEND_URL || 'http://vula-ecommerce:9000', + hotel: process.env.VULA_HOTEL_BACKEND_URL || 'http://vula-hotel:9000', + restaurant: process.env.VULA_RESTAURANT_BACKEND_URL || 'http://vula-restaurant:9000', + services: process.env.VULA_SERVICES_BACKEND_URL || 'http://vula-services:9000', +} +const MEDUSA_URL = _urlMap[_st] || _urlMap.ecommerce + +function nodeRequest( + url: string, method: string, headers: Record, body?: string, timeoutMs = 15000 +): Promise<{ status: number; data: unknown }> { + return new Promise((resolve, reject) => { + const parsed = new URL(url) + const lib = parsed.protocol === 'https:' ? https : http + const req = lib.request({ + hostname: parsed.hostname, + port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), + path: parsed.pathname + parsed.search, + method, + headers: { ...headers, ...(body ? { 'content-length': Buffer.byteLength(body).toString() } : {}) }, + }, (res) => { + let raw = '' + res.setEncoding('utf8') + res.on('data', (chunk) => { raw += chunk }) + res.on('end', () => { + try { resolve({ status: res.statusCode ?? 200, data: JSON.parse(raw) }) } + catch { resolve({ status: res.statusCode ?? 200, data: {} }) } + }) + }) + req.setTimeout(timeoutMs, () => { req.destroy(new Error('timeout')) }) + req.on('error', reject) + if (body) req.write(body) + req.end() + }) +} + +async function proxy(req: NextRequest, segments: string[], method: string, body?: string) { + const search = req.nextUrl.search + const url = `${MEDUSA_URL}/${segments.join('/')}${search}` + const headers: Record = { 'content-type': 'application/json', accept: 'application/json' } + const pubKey = req.headers.get('x-publishable-api-key') || process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '' + if (pubKey) headers['x-publishable-api-key'] = pubKey + const timeoutMs = (method === 'POST' && segments.includes('carts')) ? 25000 : 15000 + try { + const { status, data } = await nodeRequest(url, method, headers, body, timeoutMs) + return NextResponse.json(data, { status }) + } catch { + return NextResponse.json({ error: 'Medusa proxy error' }, { status: 502 }) + } +} + +type RouteCtx = { params: { path: string[] } } +export async function GET(req: NextRequest, { params }: RouteCtx) { + return proxy(req, params.path, 'GET') +} +export async function POST(req: NextRequest, { params }: RouteCtx) { + return proxy(req, params.path, 'POST', await req.text()) +} +export async function PUT(req: NextRequest, { params }: RouteCtx) { + return proxy(req, params.path, 'PUT', await req.text()) +} +export async function PATCH(req: NextRequest, { params }: RouteCtx) { + return proxy(req, params.path, 'PATCH', await req.text()) +} +export async function DELETE(req: NextRequest, { params }: RouteCtx) { + return proxy(req, params.path, 'DELETE') +}