From 83260d0af6302bdc4fa27bbf106154e3ded282cf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 7 Mar 2025 16:55:35 -0800 Subject: [PATCH] feat(webhooks): testing whatsapp webhook in prod --- app/api/webhooks/trigger/route.ts | 104 +++++- app/api/webhooks/trigger/whatsapp/route.ts | 1 - .../sub-block/components/webhook-config.tsx | 81 ++++- components/ui/webhook-modal.tsx | 313 +++++++++++++----- 4 files changed, 414 insertions(+), 85 deletions(-) diff --git a/app/api/webhooks/trigger/route.ts b/app/api/webhooks/trigger/route.ts index ea06d3c90..2d04b38c0 100644 --- a/app/api/webhooks/trigger/route.ts +++ b/app/api/webhooks/trigger/route.ts @@ -115,6 +115,106 @@ export async function POST(request: NextRequest) { // Add GET method to handle verification for providers like WhatsApp export async function GET(request: NextRequest) { - // Reuse the same logic for verification - return POST(request) + try { + // Get the webhook path from the URL + const url = new URL(request.url) + const path = url.pathname.replace('/api/webhooks/trigger', '') + + if (!path || path === '/') { + return new NextResponse('Invalid webhook path', { status: 400 }) + } + + // Find the webhook in the database + const webhooks = await db + .select({ + webhook: webhook, + workflow: workflow, + }) + .from(webhook) + .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) + .where(and(eq(webhook.path, path), eq(webhook.isActive, true), eq(workflow.isDeployed, true))) + .limit(1) + + if (webhooks.length === 0) { + return new NextResponse('Webhook not found', { status: 404 }) + } + + const { webhook: foundWebhook, workflow: foundWorkflow } = webhooks[0] + + // Handle provider-specific verification and authentication + if (foundWebhook.provider) { + const authHeader = request.headers.get('authorization') + const providerConfig = (foundWebhook.providerConfig as Record) || {} + + switch (foundWebhook.provider) { + case 'whatsapp': + // Handle WhatsApp verification + if (request.method === 'GET') { + const mode = url.searchParams.get('hub.mode') + const token = url.searchParams.get('hub.verify_token') + const challenge = url.searchParams.get('hub.challenge') + + if (mode === 'subscribe' && token) { + if (token === providerConfig.verificationToken) { + return new NextResponse(challenge, { status: 200 }) + } else { + return new NextResponse('Verification token mismatch', { status: 403 }) + } + } + } + break + + case 'github': + // GitHub doesn't require verification in this implementation + break + + case 'stripe': + // Stripe verification would go here if needed + break + + default: + // For generic webhooks, check for a token if provided in providerConfig + if (providerConfig.token) { + const providedToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null + if (!providedToken || providedToken !== providerConfig.token) { + return new NextResponse('Unauthorized', { status: 401 }) + } + } + } + } + + // Parse the request body + const body = await request.json().catch(() => ({})) + + // Create execution context with the webhook payload + const executionId = nanoid() + + // 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, + }, + }, + } + + // 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 }) + } + + return new NextResponse('Workflow state not found', { status: 500 }) + } catch (error: any) { + console.error('Error processing webhook:', error) + return new NextResponse(`Internal Server Error: ${error.message}`, { status: 500 }) + } } diff --git a/app/api/webhooks/trigger/whatsapp/route.ts b/app/api/webhooks/trigger/whatsapp/route.ts index 71eb4be0a..1718c66c8 100644 --- a/app/api/webhooks/trigger/whatsapp/route.ts +++ b/app/api/webhooks/trigger/whatsapp/route.ts @@ -37,7 +37,6 @@ export async function GET(request: NextRequest) { const verificationToken = providerConfig.verificationToken if (verificationToken && token === verificationToken) { - console.log('WhatsApp webhook verified') return new NextResponse(challenge, { status: 200 }) } } diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx index 4b7db4708..486250365 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useParams } from 'next/navigation' -import { ExternalLink } from 'lucide-react' +import { CheckCircle2, ExternalLink } from 'lucide-react' import { Button } from '@/components/ui/button' import { WebhookModal } from '@/components/ui/webhook-modal' import { useSubBlockValue } from '../hooks/use-sub-block-value' @@ -29,7 +29,9 @@ interface WebhookConfigProps { export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConfigProps) { const [isModalOpen, setIsModalOpen] = useState(false) const [isSaving, setIsSaving] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) const [error, setError] = useState(null) + const [webhookId, setWebhookId] = useState(null) const params = useParams() const workflowId = params.id as string @@ -37,12 +39,35 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf const [webhookProvider] = useSubBlockValue(blockId, 'webhookProvider') // Generate a default path based on the workflow ID if none exists - const defaultPath = `/${workflowId.substring(0, 8)}` + const defaultPath = `${workflowId.substring(0, 8)}` // Use the default path if no path is set const [webhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath') // Store provider-specific configuration const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig') + // Check if webhook exists in the database + useEffect(() => { + const checkWebhook = async () => { + try { + // Check if there's a webhook for this workflow with this path + const pathToCheck = webhookPath || defaultPath + const response = await fetch(`/api/webhooks?workflowId=${workflowId}&path=${pathToCheck}`) + if (response.ok) { + const data = await response.json() + if (data.webhooks && data.webhooks.length > 0) { + setWebhookId(data.webhooks[0].webhook.id) + } else { + setWebhookId(null) + } + } + } catch (error) { + console.error('Error checking webhook:', error) + } + } + + checkWebhook() + }, [webhookPath, workflowId, defaultPath]) + const handleOpenModal = () => { setIsModalOpen(true) setError(null) @@ -84,6 +109,9 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf throw new Error(errorData.error || 'Failed to save webhook') } + const data = await response.json() + setWebhookId(data.webhook.id) + console.log('Webhook configuration saved successfully') return true } catch (error: any) { @@ -95,6 +123,36 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf } } + const handleDeleteWebhook = async () => { + if (!webhookId) return + + try { + setIsDeleting(true) + setError(null) + + const response = await fetch(`/api/webhooks/${webhookId}`, { + method: 'DELETE', + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to delete webhook') + } + + // Clear the webhook ID + setWebhookId(null) + + console.log('Webhook deleted successfully') + return true + } catch (error: any) { + console.error('Error deleting webhook:', error) + setError(error.message || 'Failed to delete webhook') + return false + } finally { + setIsDeleting(false) + } + } + return (
{error &&
{error}
} @@ -103,10 +161,19 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf size="sm" className="w-full" onClick={handleOpenModal} - disabled={isConnecting || isSaving} + disabled={isConnecting || isSaving || isDeleting} > - - {isSaving ? 'Saving...' : 'View Webhook URL'} + {webhookId ? ( + <> + + {isSaving ? 'Saving...' : isDeleting ? 'Deleting...' : 'Webhook Connected'} + + ) : ( + <> + + {isSaving ? 'Saving...' : isDeleting ? 'Deleting...' : 'Configure Webhook'} + + )} {isModalOpen && ( @@ -117,6 +184,8 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf webhookProvider={webhookProvider || 'generic'} workflowId={workflowId} onSave={handleSaveWebhook} + onDelete={handleDeleteWebhook} + webhookId={webhookId || undefined} /> )}
diff --git a/components/ui/webhook-modal.tsx b/components/ui/webhook-modal.tsx index 59cd7a8c0..e82fd3d65 100644 --- a/components/ui/webhook-modal.tsx +++ b/components/ui/webhook-modal.tsx @@ -1,6 +1,17 @@ -import { useState } from 'react' -import { Check, Copy, X } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Check, Copy, Loader2, Trash2, X } from 'lucide-react' import { GithubIcon, StripeIcon, WhatsAppIcon } from '@/components/icons' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Dialog, @@ -35,6 +46,8 @@ interface WebhookModalProps { webhookProvider: string workflowId: string onSave?: (path: string, providerConfig: ProviderConfig) => void + onDelete?: () => void + webhookId?: string } export function WebhookModal({ @@ -44,14 +57,48 @@ export function WebhookModal({ webhookProvider, workflowId, onSave, + onDelete, + webhookId, }: WebhookModalProps) { const [copied, setCopied] = useState(null) - const [saving, setSaving] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const isConfigured = Boolean(webhookId) // Provider-specific configuration state const [whatsappVerificationToken, setWhatsappVerificationToken] = useState('') const [githubContentType, setGithubContentType] = useState('application/json') + // Load existing configuration values + useEffect(() => { + if (webhookId) { + // If we have a webhook ID, try to fetch the existing configuration + const fetchWebhookConfig = async () => { + try { + const response = await fetch(`/api/webhooks/${webhookId}`) + if (response.ok) { + const data = await response.json() + if (data.webhook?.webhook?.providerConfig) { + const config = data.webhook.webhook.providerConfig + + // Check provider type and set appropriate state + if (webhookProvider === 'whatsapp' && 'verificationToken' in config) { + setWhatsappVerificationToken(config.verificationToken) + } else if (webhookProvider === 'github' && 'contentType' in config) { + setGithubContentType(config.contentType) + } + } + } + } catch (error) { + console.error('Error fetching webhook config:', error) + } + } + + fetchWebhookConfig() + } + }, [webhookId, webhookProvider]) + // Format the path to ensure it starts with a slash const formattedPath = webhookPath && webhookPath.trim() !== '' @@ -88,19 +135,35 @@ export function WebhookModal({ } const handleSave = async () => { - if (onSave) { - setSaving(true) - try { + setIsSaving(true) + try { + // Call the onSave callback with the path and provider-specific config + if (onSave) { const providerConfig = getProviderConfig() - await onSave(webhookPath || formattedPath.substring(1), providerConfig) - onClose() - } catch (error) { - console.error('Error saving webhook configuration:', error) - } finally { - setSaving(false) + // Use the path without the leading slash + const pathToSave = formattedPath.startsWith('/') + ? formattedPath.substring(1) + : formattedPath + await onSave(pathToSave, providerConfig) } - } else { - onClose() + } catch (error) { + console.error('Error saving webhook:', error) + } finally { + setIsSaving(false) + } + } + + const handleDelete = async () => { + setIsDeleting(true) + try { + if (onDelete) { + await onDelete() + setShowDeleteConfirm(false) + } + } catch (error) { + console.error('Error deleting webhook:', error) + } finally { + setIsDeleting(false) } } @@ -140,15 +203,59 @@ export function WebhookModal({

Setup Instructions

-
    -
  1. Go to your Meta for Developers dashboard
  2. -
  3. Navigate to your WhatsApp app settings
  4. -
  5. Under "Webhooks", click "Configure"
  6. -
  7. Enter the Webhook URL shown above
  8. -
  9. Enter the verification token you specified above
  10. -
  11. Subscribe to the "messages" webhook field
  12. -
  13. Save your changes
  14. +
      +
    1. + 1. + Go to WhatsApp Business Platform dashboard +
    2. +
    3. + 2. + Navigate to "Configuration" in the sidebar +
    4. +
    5. + 3. + Enter the URL above as "Callback URL" +
    6. +
    7. + 4. + Enter your token as "Verify token" +
    8. +
    9. + 5. + + Click "Verify and save" and subscribe to "messages" + +
    +
    +
    Requirements
    +
      +
    • + + + URL must be publicly accessible with HTTPS + +
    • +
    • + + + Self-signed SSL certificates not supported + +
    • +
    • + + + For local testing, use ngrok to expose your server + +
    • +
    +
    +
    +

    + 💡 + After saving, use "Test" to verify your webhook connection. +

    +
) @@ -205,64 +312,118 @@ export function WebhookModal({ } return ( - !open && onClose()}> - - - {getProviderIcon()} -
- Webhook Configuration - - Configure your{' '} - {webhookProvider === 'whatsapp' - ? 'WhatsApp' - : webhookProvider === 'github' - ? 'GitHub' - : webhookProvider === 'stripe' - ? 'Stripe' - : 'webhook'}{' '} - integration - -
-
- -
-
- -
- - +
+
+ + {renderProviderContent()} +
+ + + {webhookId && ( +
+ + {webhookProvider === 'whatsapp' && ( + + )} +
+ )} +
+ +
- +
+
+
- {/* Provider-specific instructions and configuration */} -
{renderProviderContent()}
- - - - -
- - -
-
- - + + + + Are you sure? + + This will delete the webhook configuration. This action cannot be undone. + + + + Cancel + + {isDeleting ? 'Deleting...' : 'Delete'} + + + + + ) }