Browse Source

feat: add ShopRenter API proxy Edge Function #55

Claude 5 months ago
parent
commit
bf095ed9aa
1 changed files with 158 additions and 0 deletions
  1. 158 0
      supabase/functions/shoprenter-proxy/index.ts

+ 158 - 0
supabase/functions/shoprenter-proxy/index.ts

@@ -0,0 +1,158 @@
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { wrapHandler } from '../_shared/error-handler.ts'
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-shoprenter-shop',
+  'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
+}
+
+/**
+ * ShopRenter API Proxy
+ *
+ * This Edge Function acts as a transparent proxy to the ShopRenter API.
+ * It solves the HTTP version compatibility issue where n8n uses HTTP/2+
+ * but ShopRenter API requires HTTP/1.0.
+ *
+ * Usage from n8n:
+ * 1. Set Authorization header with Bearer token (from ShopRenter OAuth)
+ * 2. Set X-ShopRenter-Shop header with shop name (e.g., "myshop")
+ * 3. Make requests to: /shoprenter-proxy/api/{endpoint}
+ * 4. The proxy will forward to: https://{shop}.shoprenter.hu/api/{endpoint}
+ *
+ * All request methods (GET, POST, PUT, PATCH, DELETE) are supported.
+ * All headers (except Host) are forwarded transparently.
+ * Request and response bodies are proxied as-is.
+ */
+
+serve(wrapHandler('shoprenter-proxy', async (req) => {
+  // Handle CORS preflight
+  if (req.method === 'OPTIONS') {
+    return new Response('ok', { headers: corsHeaders })
+  }
+
+  try {
+    // Extract shop name from custom header
+    const shopName = req.headers.get('X-ShopRenter-Shop')
+    if (!shopName) {
+      return new Response(
+        JSON.stringify({
+          error: 'Missing X-ShopRenter-Shop header',
+          message: 'Please provide the shop name in the X-ShopRenter-Shop header (e.g., "myshop" for myshop.shoprenter.hu)'
+        }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      )
+    }
+
+    // Extract API endpoint from URL path
+    const url = new URL(req.url)
+    const pathMatch = url.pathname.match(/\/shoprenter-proxy(\/.*)$/)
+
+    if (!pathMatch || !pathMatch[1]) {
+      return new Response(
+        JSON.stringify({
+          error: 'Invalid API endpoint',
+          message: 'Request path must be in format: /shoprenter-proxy/api/{endpoint}'
+        }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      )
+    }
+
+    const apiPath = pathMatch[1]
+
+    // Construct ShopRenter API URL
+    const shopRenterUrl = `https://${shopName}.shoprenter.hu${apiPath}${url.search}`
+
+    console.log(`[ShopRenter Proxy] Forwarding ${req.method} request to: ${shopRenterUrl}`)
+
+    // Prepare headers for ShopRenter API request
+    const shopRenterHeaders = new Headers()
+
+    // Copy all headers from the original request except Host and custom headers
+    for (const [key, value] of req.headers.entries()) {
+      const lowerKey = key.toLowerCase()
+      if (lowerKey !== 'host' && !lowerKey.startsWith('x-shoprenter-')) {
+        shopRenterHeaders.set(key, value)
+      }
+    }
+
+    // Ensure required headers are present
+    if (!shopRenterHeaders.has('Content-Type')) {
+      shopRenterHeaders.set('Content-Type', 'application/json')
+    }
+    if (!shopRenterHeaders.has('Accept')) {
+      shopRenterHeaders.set('Accept', 'application/json')
+    }
+
+    // Read request body if present
+    let body = null
+    if (req.method !== 'GET' && req.method !== 'HEAD') {
+      const contentType = req.headers.get('Content-Type') || ''
+      if (contentType.includes('application/json')) {
+        try {
+          body = await req.text()
+        } catch (e) {
+          console.error('[ShopRenter Proxy] Error reading request body:', e)
+        }
+      }
+    }
+
+    // Make request to ShopRenter API using fetch
+    // Deno's fetch implementation will use HTTP/1.1 by default
+    const shopRenterResponse = await fetch(shopRenterUrl, {
+      method: req.method,
+      headers: shopRenterHeaders,
+      body: body,
+      // Force HTTP/1.1 to avoid ShopRenter API issues
+      // Note: This is handled by Deno's fetch by default
+    })
+
+    console.log(`[ShopRenter Proxy] ShopRenter API responded with status: ${shopRenterResponse.status}`)
+
+    // Prepare response headers
+    const responseHeaders = new Headers(corsHeaders)
+
+    // Copy relevant headers from ShopRenter response
+    for (const [key, value] of shopRenterResponse.headers.entries()) {
+      const lowerKey = key.toLowerCase()
+      // Copy headers but skip transfer-encoding and connection as they're handled by the Edge Function
+      if (lowerKey !== 'transfer-encoding' && lowerKey !== 'connection') {
+        responseHeaders.set(key, value)
+      }
+    }
+
+    // Read response body
+    const responseBody = await shopRenterResponse.text()
+
+    // Return proxied response
+    return new Response(
+      responseBody,
+      {
+        status: shopRenterResponse.status,
+        statusText: shopRenterResponse.statusText,
+        headers: responseHeaders
+      }
+    )
+
+  } catch (error) {
+    console.error('[ShopRenter Proxy] Error:', error)
+
+    return new Response(
+      JSON.stringify({
+        error: 'Proxy error',
+        message: error instanceof Error ? error.message : 'Unknown error occurred',
+        details: error instanceof Error ? error.stack : undefined
+      }),
+      {
+        status: 500,
+        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+      }
+    )
+  }
+}))