improvement(webhooks): consolidated webhook routes

This commit is contained in:
Waleed Latif
2025-03-09 04:21:27 -07:00
parent 6b10c8fedc
commit 3bb6b5bf02
9 changed files with 392 additions and 846 deletions

View File

@@ -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<string, any>) || {}
// 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<string, string> = {
'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 })
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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<string, any>) || {}
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 }
)
}
}

View File

@@ -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<string, any>) || {}
// 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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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<string, any>) || {}
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 }
)
}
}

View File

@@ -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<string, any>) || {}
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<string, any>) || {}
@@ -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<string, string> = {}
if (userEnv) {
for (const [key, encryptedValue] of Object.entries(
userEnv.variables as Record<string, string>
)) {
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 })
}
}

View File

@@ -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<string, any>) || {}
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<string, string> = {}
if (userEnv) {
for (const [key, encryptedValue] of Object.entries(
userEnv.variables as Record<string, string>
)) {
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 })
}
}

View File

@@ -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({