From 3bb6b5bf022304da2c5ba29e149acb0ba9c61615 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 9 Mar 2025 04:21:27 -0700 Subject: [PATCH] improvement(webhooks): consolidated webhook routes --- app/api/webhooks/[id]/test/route.ts | 154 ------------ app/api/webhooks/test/generic/route.ts | 53 ---- app/api/webhooks/test/github/route.ts | 68 ----- app/api/webhooks/test/route.ts | 178 +++++++++++++ app/api/webhooks/test/stripe/route.ts | 63 ----- app/api/webhooks/test/whatsapp/route.ts | 117 --------- app/api/webhooks/trigger/[path]/route.ts | 282 +++++++++++++++------ app/api/webhooks/whatsapp/route.ts | 302 ----------------------- components/ui/webhook-modal.tsx | 21 +- 9 files changed, 392 insertions(+), 846 deletions(-) delete mode 100644 app/api/webhooks/[id]/test/route.ts delete mode 100644 app/api/webhooks/test/generic/route.ts delete mode 100644 app/api/webhooks/test/github/route.ts create mode 100644 app/api/webhooks/test/route.ts delete mode 100644 app/api/webhooks/test/stripe/route.ts delete mode 100644 app/api/webhooks/test/whatsapp/route.ts delete mode 100644 app/api/webhooks/whatsapp/route.ts diff --git a/app/api/webhooks/[id]/test/route.ts b/app/api/webhooks/[id]/test/route.ts deleted file mode 100644 index d52914b01..000000000 --- a/app/api/webhooks/[id]/test/route.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { and, eq } from 'drizzle-orm' -import { getSession } from '@/lib/auth' -import { db } from '@/db' -import { webhook, workflow } from '@/db/schema' - -export const dynamic = 'force-dynamic' - -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - try { - const { id } = await params - - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - // Find the webhook and check ownership - const webhooks = await db - .select({ - webhook: webhook, - workflow: { - id: workflow.id, - name: workflow.name, - userId: workflow.userId, - }, - }) - .from(webhook) - .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) - .where(eq(webhook.id, id)) - .limit(1) - - if (webhooks.length === 0) { - return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) - } - - if (webhooks[0].workflow.userId !== session.user.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) - } - - const foundWebhook = webhooks[0].webhook - const providerConfig = (foundWebhook.providerConfig as Record) || {} - - // Create a test payload based on the webhook provider - let testPayload = {} - - switch (foundWebhook.provider) { - case 'whatsapp': - testPayload = { - entry: [ - { - changes: [ - { - value: { - metadata: { - phone_number_id: '123456789', - }, - messages: [ - { - from: '9876543210', - id: 'test-message-id', - timestamp: new Date().toISOString(), - text: { - body: 'This is a test message from the webhook test endpoint', - }, - }, - ], - }, - }, - ], - }, - ], - } - break - case 'github': - testPayload = { - action: 'test', - repository: { - full_name: 'user/repo', - }, - sender: { - login: 'testuser', - }, - } - break - case 'stripe': - testPayload = { - id: 'evt_test', - type: 'test.webhook', - created: Math.floor(Date.now() / 1000), - data: { - object: { - id: 'test_obj_123', - }, - }, - } - break - default: - testPayload = { - event: 'test', - timestamp: new Date().toISOString(), - data: { - message: 'This is a test webhook event', - }, - } - } - - // Make a request to the webhook trigger endpoint - const baseUrl = new URL(request.url).origin - const webhookPath = foundWebhook.path.startsWith('/') - ? foundWebhook.path - : `/${foundWebhook.path}` - const triggerUrl = `${baseUrl}/api/webhooks/trigger${webhookPath}` - - const headers: Record = { - 'Content-Type': 'application/json', - } - - // Add provider-specific headers - if (foundWebhook.provider === 'whatsapp' && providerConfig.verificationToken) { - // For WhatsApp, we don't need to add any headers for the test - } else if (foundWebhook.provider === 'github' && providerConfig.contentType) { - headers['Content-Type'] = providerConfig.contentType - } else if (providerConfig.token) { - // For generic webhooks with a token - headers['Authorization'] = `Bearer ${providerConfig.token}` - } - - try { - const response = await fetch(triggerUrl, { - method: 'POST', - headers, - body: JSON.stringify(testPayload), - }) - - const responseData = await response.json().catch(() => ({})) - - return NextResponse.json({ - success: response.ok, - status: response.status, - statusText: response.statusText, - data: responseData, - }) - } catch (error: any) { - return NextResponse.json({ - success: false, - error: error.message || 'Failed to trigger webhook', - }) - } - } catch (error: any) { - console.error('Error testing webhook:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -} diff --git a/app/api/webhooks/test/generic/route.ts b/app/api/webhooks/test/generic/route.ts deleted file mode 100644 index a883428ec..000000000 --- a/app/api/webhooks/test/generic/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { and, eq } from 'drizzle-orm' -import { db } from '@/db' -import { webhook } from '@/db/schema' - -export const dynamic = 'force-dynamic' - -export async function GET(request: NextRequest) { - try { - // Get the webhook ID from the query parameters - const { searchParams } = new URL(request.url) - const webhookId = searchParams.get('id') - - if (!webhookId) { - return NextResponse.json({ success: false, error: 'Webhook ID is required' }, { status: 400 }) - } - - // Find the webhook in the database - const webhooks = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1) - - if (webhooks.length === 0) { - return NextResponse.json({ success: false, error: 'Webhook not found' }, { status: 404 }) - } - - const foundWebhook = webhooks[0] - - // Construct the webhook URL - const baseUrl = new URL(request.url).origin - const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}` - - // Return the webhook information - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - provider: foundWebhook.provider, - isActive: foundWebhook.isActive, - }, - message: 'Webhook configuration is valid. You can use this URL to receive webhook events.', - }) - } catch (error: any) { - console.error('Error testing webhook:', error) - return NextResponse.json( - { - success: false, - error: 'Test failed', - message: error.message, - }, - { status: 500 } - ) - } -} diff --git a/app/api/webhooks/test/github/route.ts b/app/api/webhooks/test/github/route.ts deleted file mode 100644 index 003b3220a..000000000 --- a/app/api/webhooks/test/github/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { and, eq } from 'drizzle-orm' -import { db } from '@/db' -import { webhook } from '@/db/schema' - -export const dynamic = 'force-dynamic' - -export async function GET(request: NextRequest) { - try { - // Get the webhook ID from the query parameters - const { searchParams } = new URL(request.url) - const webhookId = searchParams.get('id') - - if (!webhookId) { - return NextResponse.json({ success: false, error: 'Webhook ID is required' }, { status: 400 }) - } - - // Find the webhook in the database - const webhooks = await db - .select() - .from(webhook) - .where(and(eq(webhook.id, webhookId), eq(webhook.provider, 'github'))) - .limit(1) - - if (webhooks.length === 0) { - return NextResponse.json( - { success: false, error: 'GitHub webhook not found' }, - { status: 404 } - ) - } - - const foundWebhook = webhooks[0] - const providerConfig = (foundWebhook.providerConfig as Record) || {} - const contentType = providerConfig.contentType || 'application/json' - - // Construct the webhook URL - const baseUrl = new URL(request.url).origin - const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}` - - // Return the webhook information - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - contentType, - isActive: foundWebhook.isActive, - }, - message: - 'GitHub webhook configuration is valid. Use this URL in your GitHub repository settings.', - setup: { - url: webhookUrl, - contentType, - events: ['push', 'pull_request', 'issues', 'issue_comment'], - }, - }) - } catch (error: any) { - console.error('Error testing GitHub webhook:', error) - return NextResponse.json( - { - success: false, - error: 'Test failed', - message: error.message, - }, - { status: 500 } - ) - } -} diff --git a/app/api/webhooks/test/route.ts b/app/api/webhooks/test/route.ts new file mode 100644 index 000000000..db27462a5 --- /dev/null +++ b/app/api/webhooks/test/route.ts @@ -0,0 +1,178 @@ +import { NextRequest, NextResponse } from 'next/server' +import { and, eq } from 'drizzle-orm' +import { db } from '@/db' +import { webhook } from '@/db/schema' + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + // Get the webhook ID and provider from the query parameters + const { searchParams } = new URL(request.url) + const webhookId = searchParams.get('id') + + if (!webhookId) { + return NextResponse.json({ success: false, error: 'Webhook ID is required' }, { status: 400 }) + } + + // Find the webhook in the database + const webhooks = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1) + + if (webhooks.length === 0) { + return NextResponse.json({ success: false, error: 'Webhook not found' }, { status: 404 }) + } + + const foundWebhook = webhooks[0] + const provider = foundWebhook.provider || 'generic' + const providerConfig = (foundWebhook.providerConfig as Record) || {} + + // Construct the webhook URL + const baseUrl = new URL(request.url).origin + const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}` + + // Provider-specific test logic + switch (provider) { + case 'whatsapp': { + const verificationToken = providerConfig.verificationToken + + if (!verificationToken) { + return NextResponse.json( + { success: false, error: 'Webhook has no verification token' }, + { status: 400 } + ) + } + + // Generate a test challenge + const challenge = `test_${Date.now()}` + + // Construct the WhatsApp verification URL + const whatsappUrl = `${webhookUrl}?hub.mode=subscribe&hub.verify_token=${verificationToken}&hub.challenge=${challenge}` + + console.log('Testing WhatsApp webhook:', { + webhookId, + url: whatsappUrl, + token: verificationToken ? verificationToken.substring(0, 3) + '***' : null, + }) + + // Make a request to the webhook endpoint + const response = await fetch(whatsappUrl, { + headers: { + 'User-Agent': 'facebookplatform/1.0', + }, + }) + + // Get the response details + const status = response.status + const contentType = response.headers.get('content-type') + const responseText = await response.text() + + console.log('WhatsApp test response:', { + status, + contentType, + responseText, + }) + + // Check if the test was successful + const success = status === 200 && responseText === challenge + + return NextResponse.json({ + success, + webhook: { + id: foundWebhook.id, + url: webhookUrl, + verificationToken, + isActive: foundWebhook.isActive, + }, + test: { + status, + contentType, + responseText, + expectedStatus: 200, + expectedContentType: 'text/plain', + expectedResponse: challenge, + }, + message: success + ? 'Webhook configuration is valid. You can now use this URL in WhatsApp.' + : 'Webhook verification failed. Please check your configuration.', + diagnostics: { + statusMatch: status === 200 ? '✅ Status code is 200' : '❌ Status code should be 200', + contentTypeMatch: + contentType === 'text/plain' + ? '✅ Content-Type is text/plain' + : '❌ Content-Type should be text/plain', + bodyMatch: + responseText === challenge + ? '✅ Response body matches challenge' + : '❌ Response body should exactly match the challenge string', + }, + }) + } + + case 'github': { + const contentType = providerConfig.contentType || 'application/json' + + return NextResponse.json({ + success: true, + webhook: { + id: foundWebhook.id, + url: webhookUrl, + contentType, + isActive: foundWebhook.isActive, + }, + message: + 'GitHub webhook configuration is valid. Use this URL in your GitHub repository settings.', + setup: { + url: webhookUrl, + contentType, + events: ['push', 'pull_request', 'issues', 'issue_comment'], + }, + }) + } + + case 'stripe': { + return NextResponse.json({ + success: true, + webhook: { + id: foundWebhook.id, + url: webhookUrl, + isActive: foundWebhook.isActive, + }, + message: 'Stripe webhook configuration is valid. Use this URL in your Stripe dashboard.', + setup: { + url: webhookUrl, + events: [ + 'charge.succeeded', + 'invoice.payment_succeeded', + 'customer.subscription.created', + ], + }, + }) + } + + default: { + // Generic webhook test + return NextResponse.json({ + success: true, + webhook: { + id: foundWebhook.id, + url: webhookUrl, + provider: foundWebhook.provider, + isActive: foundWebhook.isActive, + }, + message: + 'Webhook configuration is valid. You can use this URL to receive webhook events.', + }) + } + } + } catch (error: any) { + console.error('Error testing webhook:', error) + return NextResponse.json( + { + success: false, + error: 'Test failed', + message: error.message, + }, + { status: 500 } + ) + } +} diff --git a/app/api/webhooks/test/stripe/route.ts b/app/api/webhooks/test/stripe/route.ts deleted file mode 100644 index 6d051f5f7..000000000 --- a/app/api/webhooks/test/stripe/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { and, eq } from 'drizzle-orm' -import { db } from '@/db' -import { webhook } from '@/db/schema' - -export const dynamic = 'force-dynamic' - -export async function GET(request: NextRequest) { - try { - // Get the webhook ID from the query parameters - const { searchParams } = new URL(request.url) - const webhookId = searchParams.get('id') - - if (!webhookId) { - return NextResponse.json({ success: false, error: 'Webhook ID is required' }, { status: 400 }) - } - - // Find the webhook in the database - const webhooks = await db - .select() - .from(webhook) - .where(and(eq(webhook.id, webhookId), eq(webhook.provider, 'stripe'))) - .limit(1) - - if (webhooks.length === 0) { - return NextResponse.json( - { success: false, error: 'Stripe webhook not found' }, - { status: 404 } - ) - } - - const foundWebhook = webhooks[0] - - // Construct the webhook URL - const baseUrl = new URL(request.url).origin - const webhookUrl = `${baseUrl}/api/webhooks/trigger/${foundWebhook.path}` - - // Return the webhook information - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - isActive: foundWebhook.isActive, - }, - message: 'Stripe webhook configuration is valid. Use this URL in your Stripe dashboard.', - setup: { - url: webhookUrl, - events: ['charge.succeeded', 'invoice.payment_succeeded', 'customer.subscription.created'], - }, - }) - } catch (error: any) { - console.error('Error testing Stripe webhook:', error) - return NextResponse.json( - { - success: false, - error: 'Test failed', - message: error.message, - }, - { status: 500 } - ) - } -} diff --git a/app/api/webhooks/test/whatsapp/route.ts b/app/api/webhooks/test/whatsapp/route.ts deleted file mode 100644 index e143d2d5d..000000000 --- a/app/api/webhooks/test/whatsapp/route.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { and, eq } from 'drizzle-orm' -import { db } from '@/db' -import { webhook } from '@/db/schema' - -export const dynamic = 'force-dynamic' - -export async function GET(request: NextRequest) { - try { - // Get the webhook ID from the query parameters - const { searchParams } = new URL(request.url) - const webhookId = searchParams.get('id') - - if (!webhookId) { - return NextResponse.json({ success: false, error: 'Webhook ID is required' }, { status: 400 }) - } - - // Find the webhook in the database - const webhooks = await db - .select() - .from(webhook) - .where(and(eq(webhook.id, webhookId), eq(webhook.provider, 'whatsapp'))) - .limit(1) - - if (webhooks.length === 0) { - return NextResponse.json({ success: false, error: 'Webhook not found' }, { status: 404 }) - } - - const foundWebhook = webhooks[0] - const providerConfig = (foundWebhook.providerConfig as Record) || {} - const verificationToken = providerConfig.verificationToken - - if (!verificationToken) { - return NextResponse.json( - { success: false, error: 'Webhook has no verification token' }, - { status: 400 } - ) - } - - // Generate a test challenge - const challenge = `test_${Date.now()}` - - // Construct the WhatsApp verification URL - const baseUrl = new URL(request.url).origin - const whatsappUrl = `${baseUrl}/api/webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=${verificationToken}&hub.challenge=${challenge}` - - console.log('Testing WhatsApp webhook:', { - webhookId, - url: whatsappUrl, - token: verificationToken ? verificationToken.substring(0, 3) + '***' : null, - }) - - // Make a request to the WhatsApp webhook endpoint - const response = await fetch(whatsappUrl, { - headers: { - 'User-Agent': 'facebookplatform/1.0', - }, - }) - - // Get the response details - const status = response.status - const contentType = response.headers.get('content-type') - const responseText = await response.text() - - console.log('WhatsApp test response:', { - status, - contentType, - responseText, - }) - - // Check if the test was successful - const success = status === 200 && responseText === challenge - - // Return the test results - return NextResponse.json({ - success, - webhook: { - id: foundWebhook.id, - url: `${baseUrl}/api/webhooks/whatsapp`, - verificationToken, - isActive: foundWebhook.isActive, - }, - test: { - status, - contentType, - responseText, - expectedStatus: 200, - expectedContentType: 'text/plain', - expectedResponse: challenge, - }, - message: success - ? 'Webhook configuration is valid. You can now use this URL in WhatsApp.' - : 'Webhook verification failed. Please check your configuration.', - diagnostics: { - statusMatch: status === 200 ? '✅ Status code is 200' : '❌ Status code should be 200', - contentTypeMatch: - contentType === 'text/plain' - ? '✅ Content-Type is text/plain' - : '❌ Content-Type should be text/plain', - bodyMatch: - responseText === challenge - ? '✅ Response body matches challenge' - : '❌ Response body should exactly match the challenge string', - }, - }) - } catch (error: any) { - console.error('Error testing WhatsApp webhook:', error) - return NextResponse.json( - { - success: false, - error: 'Test failed', - message: error.message, - }, - { status: 500 } - ) - } -} diff --git a/app/api/webhooks/trigger/[path]/route.ts b/app/api/webhooks/trigger/[path]/route.ts index e467303d9..091feba0c 100644 --- a/app/api/webhooks/trigger/[path]/route.ts +++ b/app/api/webhooks/trigger/[path]/route.ts @@ -1,18 +1,99 @@ import { NextRequest, NextResponse } from 'next/server' import { and, eq } from 'drizzle-orm' -import { nanoid } from 'nanoid' +import { v4 as uuidv4 } from 'uuid' +import { persistLog } from '@/lib/logging' +import { decryptSecret } from '@/lib/utils' +import { mergeSubblockState } from '@/stores/workflows/utils' import { db } from '@/db' -import { webhook, workflow } from '@/db/schema' +import { environment, webhook, workflow } from '@/db/schema' import { Executor } from '@/executor' +import { Serializer } from '@/serializer' import { SerializedWorkflow } from '@/serializer/types' export const dynamic = 'force-dynamic' /** - * Generic webhook trigger endpoint for non-WhatsApp providers - * Handles webhooks with a path parameter + * Consolidated webhook trigger endpoint for all providers + * Handles both WhatsApp verification and other webhook providers */ +export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string }> }) { + try { + const path = (await params).path + const url = new URL(request.url) + + // Check if this is a WhatsApp verification request + const mode = url.searchParams.get('hub.mode') + const token = url.searchParams.get('hub.verify_token') + const challenge = url.searchParams.get('hub.challenge') + + if (mode && token && challenge) { + // This is a WhatsApp verification request + console.log('WhatsApp verification request received') + + if (mode !== 'subscribe') { + console.log('Invalid mode:', mode) + return new NextResponse('Invalid mode', { status: 400 }) + } + + // Find all active WhatsApp webhooks + const webhooks = await db + .select() + .from(webhook) + .where(and(eq(webhook.provider, 'whatsapp'), eq(webhook.isActive, true))) + + // Check if any webhook has a matching verification token + for (const wh of webhooks) { + const providerConfig = (wh.providerConfig as Record) || {} + const verificationToken = providerConfig.verificationToken + + if (!verificationToken) { + console.log(`Webhook ${wh.id} has no verification token, skipping`) + continue + } + + if (token === verificationToken) { + console.log( + `Verification successful for webhook ${wh.id}, returning challenge: ${challenge}` + ) + // Return ONLY the challenge as plain text (exactly as WhatsApp expects) + return new NextResponse(challenge, { + status: 200, + headers: { + 'Content-Type': 'text/plain', + }, + }) + } + } + + console.log('No matching verification token found') + return new NextResponse('Verification failed', { status: 403 }) + } + + // For non-WhatsApp verification requests + console.log('Looking for webhook with path:', path) + + // Find the webhook in the database + const webhooks = await db + .select({ + webhook: webhook, + }) + .from(webhook) + .where(and(eq(webhook.path, path), eq(webhook.isActive, true))) + .limit(1) + + if (webhooks.length === 0) { + return new NextResponse('Webhook not found', { status: 404 }) + } + + // For other providers, just return a 200 OK + return new NextResponse('OK', { status: 200 }) + } catch (error: any) { + console.error('Error processing webhook verification:', error) + return new NextResponse(`Internal Server Error: ${error.message}`, { status: 500 }) + } +} + export async function POST( request: NextRequest, { params }: { params: Promise<{ path: string }> } @@ -20,7 +101,9 @@ export async function POST( try { const path = (await params).path - console.log('Looking for webhook with path:', path) + // Parse the request body + const body = await request.json().catch(() => ({})) + console.log(`Webhook POST request received for path: ${path}`) // Find the webhook in the database const webhooks = await db @@ -38,16 +121,10 @@ export async function POST( } const { webhook: foundWebhook, workflow: foundWorkflow } = webhooks[0] - - // Skip WhatsApp webhooks - they should use the dedicated endpoint - if (foundWebhook.provider === 'whatsapp') { - return new NextResponse('WhatsApp webhooks should use the dedicated endpoint', { - status: 400, - }) - } + const executionId = uuidv4() // Handle provider-specific verification and authentication - if (foundWebhook.provider) { + if (foundWebhook.provider && foundWebhook.provider !== 'whatsapp') { const authHeader = request.headers.get('authorization') const providerConfig = (foundWebhook.providerConfig as Record) || {} @@ -71,74 +148,135 @@ export async function POST( } } - // Parse the request body - const body = await request.json().catch(() => ({})) + // Format the input based on provider + let input = {} - // Create execution context with the webhook payload - const executionId = nanoid() + if (foundWebhook.provider === 'whatsapp') { + // Extract WhatsApp specific data + const data = body?.entry?.[0]?.changes?.[0]?.value + const messages = data?.messages || [] - // Format the input to match the expected BlockOutput type - const input = { - webhook: { - data: { - path, - provider: foundWebhook.provider, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, + if (messages.length > 0) { + const message = messages[0] + const phoneNumberId = data.metadata?.phone_number_id + const from = message.from + const messageId = message.id + const timestamp = message.timestamp + const text = message.text?.body + + console.log( + `Received WhatsApp message: ${text ? text.substring(0, 50) : '[no text]'} from ${from}` + ) + + input = { + whatsapp: { + data: { + messageId, + from, + phoneNumberId, + text, + timestamp, + raw: message, + }, + }, + webhook: { + data: { + provider: 'whatsapp', + path: foundWebhook.path, + providerConfig: foundWebhook.providerConfig, + payload: body, + headers: Object.fromEntries(request.headers.entries()), + method: request.method, + }, + }, + } + } else { + // This might be a different type of notification (e.g., status update) + console.log('No messages in WhatsApp payload, might be a status update') + return new NextResponse('OK', { status: 200 }) + } + } else { + // Generic format for other providers + input = { + webhook: { + data: { + path, + provider: foundWebhook.provider, + providerConfig: foundWebhook.providerConfig, + payload: body, + headers: Object.fromEntries(request.headers.entries()), + method: request.method, + }, }, - }, + } } - // Execute the workflow - if (foundWorkflow.state) { - const executor = new Executor(foundWorkflow.state as SerializedWorkflow, input) - const result = await executor.execute(foundWorkflow.id) - - // Return the execution result - return NextResponse.json(result, { status: 200 }) + // Get the workflow state + if (!foundWorkflow.state) { + console.log(`Workflow ${foundWorkflow.id} has no state, skipping`) + return new NextResponse('Workflow state not found', { status: 500 }) } - return new NextResponse('Workflow state not found', { status: 500 }) + console.log(`Executing workflow ${foundWorkflow.id} for webhook ${foundWebhook.id}`) + + // Get the workflow state + const state = foundWorkflow.state as any + const { blocks, edges, loops } = state + + // Use the same execution flow as in manual executions + const mergedStates = mergeSubblockState(blocks) + + // Retrieve environment variables for this user + const [userEnv] = await db + .select() + .from(environment) + .where(eq(environment.userId, foundWorkflow.userId)) + .limit(1) + + // Create a map of decrypted environment variables + const decryptedEnvVars: Record = {} + if (userEnv) { + for (const [key, encryptedValue] of Object.entries( + userEnv.variables as Record + )) { + try { + const { decrypted } = await decryptSecret(encryptedValue) + decryptedEnvVars[key] = decrypted + } catch (error: any) { + console.error(`Failed to decrypt ${key}:`, error) + throw new Error(`Failed to decrypt environment variable "${key}": ${error.message}`) + } + } + } + + // Serialize and execute the workflow + const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates as any, edges, loops) + + const executor = new Executor(serializedWorkflow, mergedStates as any, decryptedEnvVars, input) + const result = await executor.execute(foundWorkflow.id) + + console.log(`Successfully executed workflow ${foundWorkflow.id}`) + + // Log each execution step + for (const log of result.logs || []) { + await persistLog({ + id: uuidv4(), + workflowId: foundWorkflow.id, + executionId, + level: log.success ? 'info' : 'error', + message: log.success + ? `Block ${log.blockName || log.blockId} (${log.blockType}): ${JSON.stringify(log.output?.response || {})}` + : `Block ${log.blockName || log.blockId} (${log.blockType}): ${log.error || 'Failed'}`, + duration: log.success ? `${log.durationMs}ms` : 'NA', + trigger: 'webhook', + createdAt: new Date(log.endedAt || log.startedAt), + }) + } + + // Return the execution result + return NextResponse.json(result, { status: 200 }) } catch (error: any) { console.error('Error processing webhook:', error) return new NextResponse(`Internal Server Error: ${error.message}`, { status: 500 }) } } - -export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string }> }) { - try { - const path = (await params).path - - console.log('Looking for webhook with path:', path) - - // Find the webhook in the database - const webhooks = await db - .select({ - webhook: webhook, - }) - .from(webhook) - .where(and(eq(webhook.path, path), eq(webhook.isActive, true))) - .limit(1) - - if (webhooks.length === 0) { - return new NextResponse('Webhook not found', { status: 404 }) - } - - const { webhook: foundWebhook } = webhooks[0] - - // Skip WhatsApp webhooks - they should use the dedicated endpoint - if (foundWebhook.provider === 'whatsapp') { - return new NextResponse('WhatsApp webhooks should use the dedicated endpoint', { - status: 400, - }) - } - - // For other providers, just return a 200 OK - return new NextResponse('OK', { status: 200 }) - } catch (error: any) { - console.error('Error processing webhook verification:', error) - return new NextResponse(`Internal Server Error: ${error.message}`, { status: 500 }) - } -} diff --git a/app/api/webhooks/whatsapp/route.ts b/app/api/webhooks/whatsapp/route.ts deleted file mode 100644 index b77c60712..000000000 --- a/app/api/webhooks/whatsapp/route.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { and, eq } from 'drizzle-orm' -import { v4 as uuidv4 } from 'uuid' -import { persistLog } from '@/lib/logging' -import { decryptSecret } from '@/lib/utils' -import { mergeSubblockState } from '@/stores/workflows/utils' -import { db } from '@/db' -import { environment, webhook, workflow } from '@/db/schema' -import { Executor } from '@/executor' -import { Serializer } from '@/serializer' - -export const dynamic = 'force-dynamic' - -/** - * WhatsApp webhook endpoint - * This follows the exact pattern in the WhatsApp sample app - * https://developers.facebook.com/docs/whatsapp/sample-app-endpoints - */ - -export async function GET(request: NextRequest) { - try { - // Log the full request details for debugging - const url = new URL(request.url) - const searchParams = Object.fromEntries(url.searchParams.entries()) - console.log('WhatsApp webhook GET request received:', { - url: url.toString(), - params: searchParams, - headers: Object.fromEntries(request.headers.entries()), - }) - - // Get the verification parameters - const mode = url.searchParams.get('hub.mode') - const token = url.searchParams.get('hub.verify_token') - const challenge = url.searchParams.get('hub.challenge') - - console.log('WhatsApp verification parameters:', { - mode, - tokenProvided: Boolean(token), - challengeProvided: Boolean(challenge), - }) - - // Validate the verification parameters - if (!mode || !token || !challenge) { - console.log('Missing verification parameters') - return new NextResponse('Missing verification parameters', { status: 400 }) - } - - if (mode !== 'subscribe') { - console.log('Invalid mode:', mode) - return new NextResponse('Invalid mode', { status: 400 }) - } - - // Find all active WhatsApp webhooks - const webhooks = await db - .select() - .from(webhook) - .where(and(eq(webhook.provider, 'whatsapp'), eq(webhook.isActive, true))) - - console.log(`Found ${webhooks.length} active WhatsApp webhooks`) - - // Check if any webhook has a matching verification token - for (const wh of webhooks) { - const providerConfig = (wh.providerConfig as Record) || {} - const verificationToken = providerConfig.verificationToken - - if (!verificationToken) { - console.log(`Webhook ${wh.id} has no verification token, skipping`) - continue - } - - console.log(`Checking webhook ${wh.id} with token ${verificationToken.substring(0, 2)}***`) - - if (token === verificationToken) { - console.log( - `Verification successful for webhook ${wh.id}, returning challenge: ${challenge}` - ) - // Return ONLY the challenge as plain text (exactly as WhatsApp expects) - return new NextResponse(challenge, { - status: 200, - headers: { - 'Content-Type': 'text/plain', - }, - }) - } - } - - console.log('No matching verification token found') - return new NextResponse('Verification failed', { status: 403 }) - } catch (error: any) { - console.error('Error processing WhatsApp webhook verification:', error) - return new NextResponse(`Internal Server Error: ${error.message}`, { status: 500 }) - } -} - -export async function POST(request: NextRequest) { - try { - console.log('WhatsApp webhook POST request received') - - // Parse the request body - const body = await request.json().catch(() => ({})) - console.log('WhatsApp webhook payload:', JSON.stringify(body).substring(0, 200) + '...') - - // Find all active WhatsApp webhooks - const webhooks = await db - .select({ - webhook: webhook, - workflow: workflow, - }) - .from(webhook) - .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) - .where( - and( - eq(webhook.provider, 'whatsapp'), - eq(webhook.isActive, true) - // No workflow.isDeployed check as we discussed - ) - ) - - if (webhooks.length === 0) { - console.log('No active WhatsApp webhooks found') - return new NextResponse('OK', { status: 200 }) - } - - console.log(`Found ${webhooks.length} active WhatsApp webhooks`) - - // Extract the WhatsApp message data - const data = body?.entry?.[0]?.changes?.[0]?.value - - if (!data) { - console.log('No data received in WhatsApp webhook') - return new NextResponse('OK', { status: 200 }) - } - - // Extract message details - const messages = data.messages || [] - - if (messages.length === 0) { - // This might be a different type of notification (e.g., status update) - console.log('No messages in WhatsApp payload, might be a status update') - return new NextResponse('OK', { status: 200 }) - } - - // Process each message - for (const message of messages) { - const phoneNumberId = data.metadata?.phone_number_id - const from = message.from - const messageId = message.id - const timestamp = message.timestamp - const text = message.text?.body - - console.log( - `Received WhatsApp message: ${text ? text.substring(0, 50) : '[no text]'} from ${from}` - ) - - // Execute each matching workflow with the WhatsApp message data - for (const { webhook: wh, workflow: wf } of webhooks) { - const executionId = uuidv4() - - try { - // Get the workflow state - if (!wf.state) { - console.log(`Workflow ${wf.id} has no state, skipping`) - continue - } - - console.log(`Executing workflow ${wf.id} for WhatsApp message ${messageId}`) - - // Create input payload for the workflow - const input = { - whatsapp: { - data: { - messageId, - from, - phoneNumberId, - text, - timestamp, - raw: message, - }, - }, - webhook: { - data: { - provider: 'whatsapp', - path: wh.path, - providerConfig: wh.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - } - - // Get the workflow state - const state = wf.state as any - const { blocks, edges, loops } = state - - // Use the same execution flow as in manual executions - const mergedStates = mergeSubblockState(blocks) - - // Retrieve environment variables for this user - const [userEnv] = await db - .select() - .from(environment) - .where(eq(environment.userId, wf.userId)) - .limit(1) - - if (!userEnv) { - console.log(`No environment variables found for user ${wf.userId}`) - } - - // Create a map of decrypted environment variables - const decryptedEnvVars: Record = {} - if (userEnv) { - for (const [key, encryptedValue] of Object.entries( - userEnv.variables as Record - )) { - try { - const { decrypted } = await decryptSecret(encryptedValue) - decryptedEnvVars[key] = decrypted - } catch (error: any) { - console.error(`Failed to decrypt ${key}:`, error) - throw new Error(`Failed to decrypt environment variable "${key}": ${error.message}`) - } - } - } - - // Serialize and execute the workflow - const serializedWorkflow = new Serializer().serializeWorkflow( - mergedStates as any, - edges, - loops - ) - - const executor = new Executor( - serializedWorkflow, - mergedStates as any, - decryptedEnvVars, - input - ) - const result = await executor.execute(wf.id) - - console.log(`Successfully executed workflow ${wf.id}`) - - // Log each execution step - for (const log of result.logs || []) { - await persistLog({ - id: uuidv4(), - workflowId: wf.id, - executionId, - level: log.success ? 'info' : 'error', - message: log.success - ? `Block ${log.blockName || log.blockId} (${log.blockType}): ${JSON.stringify(log.output?.response || {})}` - : `Block ${log.blockName || log.blockId} (${log.blockType}): ${log.error || 'Failed'}`, - duration: log.success ? `${log.durationMs}ms` : 'NA', - trigger: 'webhook', - createdAt: new Date(log.endedAt || log.startedAt), - }) - } - - // Calculate total duration from successful block logs - const totalDuration = (result.logs || []) - .filter((log) => log.success) - .reduce((sum, log) => sum + log.durationMs, 0) - - // Log the final execution result - await persistLog({ - id: uuidv4(), - workflowId: wf.id, - executionId, - level: result.success ? 'info' : 'error', - message: result.success - ? 'WhatsApp webhook executed successfully' - : `WhatsApp webhook execution failed: ${result.error}`, - duration: result.success ? `${totalDuration}ms` : 'NA', - trigger: 'webhook', - createdAt: new Date(), - }) - } catch (error: any) { - console.error(`Error executing workflow ${wf.id}:`, error) - - // Log the error - await persistLog({ - id: uuidv4(), - workflowId: wf.id, - executionId, - level: 'error', - message: `WhatsApp webhook execution failed: ${error.message || error}`, - duration: 'NA', - trigger: 'webhook', - createdAt: new Date(), - }) - } - } - } - - // Always return a 200 OK to WhatsApp - return new NextResponse('OK', { status: 200 }) - } catch (error: any) { - console.error('Error processing WhatsApp webhook:', error) - // Still return 200 to prevent WhatsApp from retrying - return new NextResponse('OK', { status: 200 }) - } -} diff --git a/components/ui/webhook-modal.tsx b/components/ui/webhook-modal.tsx index bdadba6bb..84b74e8e9 100644 --- a/components/ui/webhook-modal.tsx +++ b/components/ui/webhook-modal.tsx @@ -124,11 +124,7 @@ export function WebhookModal({ ? `${window.location.protocol}//${window.location.host}` : 'https://your-domain.com' - // Use the dedicated endpoint for WhatsApp, path-based for others - const webhookUrl = - webhookProvider === 'whatsapp' - ? `${baseUrl}/api/webhooks/whatsapp` - : `${baseUrl}/api/webhooks/trigger/${formattedPath}` + const webhookUrl = `${baseUrl}/api/webhooks/trigger/${formattedPath}` const copyToClipboard = (text: string, type: string) => { navigator.clipboard.writeText(text) @@ -192,16 +188,8 @@ export function WebhookModal({ setIsTesting(true) setTestResult(null) - // Use the provider-specific test endpoint - let testEndpoint = `/api/webhooks/test/generic?id=${webhookId}` - - if (webhookProvider === 'whatsapp') { - testEndpoint = `/api/webhooks/test/whatsapp?id=${webhookId}` - } else if (webhookProvider === 'github') { - testEndpoint = `/api/webhooks/test/github?id=${webhookId}` - } else if (webhookProvider === 'stripe') { - testEndpoint = `/api/webhooks/test/stripe?id=${webhookId}` - } + // Use the consolidated test endpoint + const testEndpoint = `/api/webhooks/test?id=${webhookId}` const response = await fetch(testEndpoint) if (!response.ok) { @@ -214,8 +202,7 @@ export function WebhookModal({ if (data.success) { setTestResult({ success: true, - message: - data.message || 'Webhook configuration is valid. You can now use this URL in WhatsApp.', + message: data.message || 'Webhook configuration is valid.', }) } else { setTestResult({