Browse Source

feat(vapi-webhook): add internal API key auth and webshop association

- Add internal API key authentication using requireInternalApiKey
- Add webshop_id query parameter validation (required UUID)
- Verify webshop exists and is active in stores table
- Associate call logs with store_id
- Remove duplicate Supabase client creation
Fszontagh 4 months ago
parent
commit
45602f0589
1 changed files with 66 additions and 5 deletions
  1. 66 5
      supabase/functions/vapi-webhook/index.ts

+ 66 - 5
supabase/functions/vapi-webhook/index.ts

@@ -1,5 +1,9 @@
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+import {
+  requireInternalApiKey,
+  createInternalApiKeyErrorResponse,
+} from '../_shared/internal-api-key-auth.ts'
 
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
@@ -78,6 +82,65 @@ serve(async (req) => {
       )
     }
 
+    // Create Supabase admin client for authentication
+    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+    const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+    const supabase = createClient(supabaseUrl, supabaseKey)
+
+    // Validate internal API key
+    const authResult = await requireInternalApiKey(req, supabase)
+    if (!authResult.valid) {
+      const errorResponse = createInternalApiKeyErrorResponse(authResult)
+      // Add CORS headers to error response
+      return new Response(errorResponse.body, {
+        status: errorResponse.status,
+        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+      })
+    }
+
+    // Extract and validate webshop_id from query parameters
+    const url = new URL(req.url)
+    const webshopId = url.searchParams.get('webshop_id')
+
+    if (!webshopId) {
+      return new Response(
+        JSON.stringify({ error: 'Missing required query parameter: webshop_id' }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // Validate UUID format
+    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
+    if (!uuidRegex.test(webshopId)) {
+      return new Response(
+        JSON.stringify({ error: 'Invalid webshop_id format. Must be a valid UUID.' }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // Verify webshop exists and is active
+    const { data: store, error: storeError } = await supabase
+      .from('stores')
+      .select('id, store_name, is_active')
+      .eq('id', webshopId)
+      .single()
+
+    if (storeError || !store) {
+      return new Response(
+        JSON.stringify({ error: 'Webshop not found', webshop_id: webshopId }),
+        { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    if (!store.is_active) {
+      return new Response(
+        JSON.stringify({ error: 'Webshop is inactive', webshop_id: webshopId }),
+        { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    console.log(`Processing VAPI webhook for webshop: ${store.store_name} (${webshopId})`)
+
     // Parse the webhook payload
     const payload: VAPIWebhookPayload = await req.json()
     console.log('Received VAPI webhook:', JSON.stringify(payload, null, 2))
@@ -94,11 +157,6 @@ serve(async (req) => {
     const { message } = payload
     const { timestamp, analysis, artifact, call } = message
 
-    // Create Supabase client with service role key (bypasses RLS for webhook)
-    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
-    const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
-    const supabase = createClient(supabaseUrl, supabaseKey)
-
     // Extract transcript from messages
     let transcript = artifact?.transcript || ''
     if (!transcript && artifact?.messages) {
@@ -133,6 +191,9 @@ serve(async (req) => {
       // Primary key - generate UUID for the call log
       id: crypto.randomUUID(),
 
+      // Associate with webshop
+      store_id: webshopId,
+
       // VAPI-specific fields
       vapi_call_id: call?.id || crypto.randomUUID(),
       vapi_timestamp: timestamp,