import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { webhook, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, } from '@sim/platform-authz/workflow' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { deleteWebhookContract, getWebhookContract, updateWebhookContract, } from '@/lib/api/contracts/webhooks' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' const logger = createLogger('WebhookAPI') export const dynamic = 'force-dynamic' // Get a specific webhook export const GET = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { const requestId = generateRequestId() try { const parsed = await parseRequest(getWebhookContract, request, context) if (!parsed.success) return parsed.response const { id } = parsed.data.params const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Unauthorized webhook access attempt`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = auth.userId const webhooks = await db .select({ webhook: webhook, workflow: { id: workflow.id, name: workflow.name, userId: workflow.userId, workspaceId: workflow.workspaceId, }, }) .from(webhook) .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) .where(and(eq(webhook.id, id), isNull(webhook.archivedAt))) .limit(1) if (webhooks.length === 0) { logger.warn(`[${requestId}] Webhook not found: ${id}`) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) } const webhookData = webhooks[0] const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId: webhookData.workflow.id, userId, action: 'read', }) const hasAccess = authorization.allowed if (!hasAccess) { logger.warn(`[${requestId}] User ${userId} denied access to webhook: ${id}`) return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } logger.info(`[${requestId}] Successfully retrieved webhook: ${id}`) return NextResponse.json({ webhook: webhooks[0] }, { status: 200 }) } catch (error) { logger.error(`[${requestId}] Error fetching webhook`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } ) export const PATCH = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { const requestId = generateRequestId() try { const parsed = await parseRequest(updateWebhookContract, request, context) if (!parsed.success) return parsed.response const { id } = parsed.data.params const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Unauthorized webhook update attempt`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = auth.userId const { isActive, failedCount } = parsed.data.body const webhooks = await db .select({ webhook: webhook, workflow: { id: workflow.id, userId: workflow.userId, workspaceId: workflow.workspaceId, }, }) .from(webhook) .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) .where(and(eq(webhook.id, id), isNull(webhook.archivedAt))) .limit(1) if (webhooks.length === 0) { logger.warn(`[${requestId}] Webhook not found: ${id}`) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) } const webhookData = webhooks[0] const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId: webhookData.workflow.id, userId, action: 'write', }) const canModify = authorization.allowed if (!canModify) { logger.warn(`[${requestId}] User ${userId} denied permission to modify webhook: ${id}`) return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } await assertWorkflowMutable(webhookData.workflow.id) const setClause: Partial = {} if (isActive !== undefined && isActive !== webhooks[0].webhook.isActive) { setClause.isActive = isActive } if (failedCount !== undefined && failedCount !== webhooks[0].webhook.failedCount) { setClause.failedCount = failedCount } if (Object.keys(setClause).length === 0) { logger.info(`[${requestId}] No-op webhook PATCH (no field changes): ${id}`) return NextResponse.json({ webhook: webhooks[0].webhook }, { status: 200 }) } setClause.updatedAt = new Date() const updatedWebhook = await db .update(webhook) .set(setClause) .where(eq(webhook.id, id)) .returning() logger.info(`[${requestId}] Successfully updated webhook: ${id}`) return NextResponse.json({ webhook: updatedWebhook[0] }, { status: 200 }) } catch (error) { if (error instanceof WorkflowLockedError) { return NextResponse.json({ error: error.message }, { status: error.status }) } logger.error(`[${requestId}] Error updating webhook`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } ) // Delete a webhook export const DELETE = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { const requestId = generateRequestId() try { const parsed = await parseRequest(deleteWebhookContract, request, context) if (!parsed.success) return parsed.response const { id } = parsed.data.params const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Unauthorized webhook deletion attempt`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = auth.userId // Find the webhook and check permissions const webhooks = await db .select({ webhook: webhook, workflow: { id: workflow.id, userId: workflow.userId, workspaceId: workflow.workspaceId, }, }) .from(webhook) .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) .where(eq(webhook.id, id)) .limit(1) if (webhooks.length === 0) { logger.warn(`[${requestId}] Webhook not found: ${id}`) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) } const webhookData = webhooks[0] const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId: webhookData.workflow.id, userId, action: 'write', }) const canDelete = authorization.allowed if (!canDelete) { logger.warn(`[${requestId}] User ${userId} denied permission to delete webhook: ${id}`) return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } await assertWorkflowMutable(webhookData.workflow.id) const foundWebhook = webhookData.webhook await cleanupExternalWebhook(foundWebhook, webhookData.workflow, requestId) await db.delete(webhook).where(eq(webhook.id, id)) try { PlatformEvents.webhookDeleted({ webhookId: id, workflowId: webhookData.workflow.id, }) } catch { // Telemetry should not fail the operation } logger.info(`[${requestId}] Successfully deleted webhook: ${id}`) recordAudit({ workspaceId: webhookData.workflow.workspaceId || null, actorId: userId, actorName: auth.userName, actorEmail: auth.userEmail, action: AuditAction.WEBHOOK_DELETED, resourceType: AuditResourceType.WEBHOOK, resourceId: id, resourceName: foundWebhook.provider || 'generic', description: `Deleted ${foundWebhook.provider || 'generic'} webhook`, metadata: { provider: foundWebhook.provider || 'generic', workflowId: webhookData.workflow.id, webhookPath: foundWebhook.path || undefined, blockId: foundWebhook.blockId || undefined, }, request, }) const wsId = webhookData.workflow.workspaceId || undefined captureServerEvent( userId, 'webhook_trigger_deleted', { webhook_id: id, workflow_id: webhookData.workflow.id, provider: foundWebhook.provider || 'generic', workspace_id: wsId ?? '', }, wsId ? { groups: { workspace: wsId } } : undefined ) return NextResponse.json({ success: true }, { status: 200 }) } catch (error: any) { if (error instanceof WorkflowLockedError) { return NextResponse.json({ error: error.message }, { status: error.status }) } logger.error(`[${requestId}] Error deleting webhook`, { error: error.message, stack: error.stack, }) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } )