From 4d6351507f133987f56873a23f3f2a259fadfee8 Mon Sep 17 00:00:00 2001 From: dekzter Date: Wed, 25 Feb 2026 10:59:45 -0500 Subject: [PATCH] enhanced webhooks to support custom headers and payload templates --- apps/connect/api_views.py | 4 +- apps/connect/handlers/webhook.py | 15 +- apps/connect/utils.py | 9 +- frontend/src/components/forms/Connection.jsx | 284 ++++++++++++++----- 4 files changed, 239 insertions(+), 73 deletions(-) diff --git a/apps/connect/api_views.py b/apps/connect/api_views.py index de3903c0..739e8e32 100644 --- a/apps/connect/api_views.py +++ b/apps/connect/api_views.py @@ -74,11 +74,13 @@ class IntegrationViewSet(viewsets.ModelViewSet): {"detail": f"Invalid event: {event}"}, status=status.HTTP_400_BAD_REQUEST, ) + # Only accept payload_template when the integration is a webhook + payload_template = item.get("payload_template") if integration.type == "webhook" else None incoming.append( { "event": event, "enabled": bool(item.get("enabled", True)), - "payload_template": item.get("payload_template"), + "payload_template": payload_template, } ) diff --git a/apps/connect/handlers/webhook.py b/apps/connect/handlers/webhook.py index ccca530a..511f0a0e 100644 --- a/apps/connect/handlers/webhook.py +++ b/apps/connect/handlers/webhook.py @@ -1,10 +1,21 @@ # connect/handlers/webhook.py -import requests +import requests, json, logging from .base import IntegrationHandler +logger = logging.getLogger(__name__) + class WebhookHandler(IntegrationHandler): def execute(self): url = self.integration.config.get("url") headers = self.integration.config.get("headers", {}) - response = requests.post(url, json=self.payload, headers=headers, timeout=10) + logger.info(self.payload) + + try: + parsed = json.loads(self.payload) + headers["Content-Type"] = "application/json" + except Exception: + pass + + response = requests.post(url, data=self.payload, headers=headers, timeout=10) + return {"status_code": response.status_code, "body": response.text, "success": response.ok} diff --git a/apps/connect/utils.py b/apps/connect/utils.py index 6144a6b3..c3a0975f 100644 --- a/apps/connect/utils.py +++ b/apps/connect/utils.py @@ -38,13 +38,14 @@ def trigger_event(event_name, payload): ) continue - # apply optional payload template + # apply optional payload template (only for webhook integrations) + # If the rendered template is valid JSON, use that object as the payload. + # Otherwise, pass the rendered string as-is. final_payload = payload - if sub.payload_template: + if integration.type == 'webhook' and sub.payload_template: try: template = Template(sub.payload_template) - rendered = template.render(Context(payload)) - final_payload = {"message": rendered} + final_payload = template.render(Context(payload)).strip() except Exception as e: logger.error( f"Payload template render failed for subscription id={sub.id}: {e}" diff --git a/frontend/src/components/forms/Connection.jsx b/frontend/src/components/forms/Connection.jsx index e8077d43..c0ec09c1 100644 --- a/frontend/src/components/forms/Connection.jsx +++ b/frontend/src/components/forms/Connection.jsx @@ -11,6 +11,11 @@ import { Checkbox, Text, SimpleGrid, + Textarea, + Group, + Tabs, + Accordion, + Alert, } from '@mantine/core'; import { isNotEmpty, useForm } from '@mantine/form'; import { SUBSCRIPTION_EVENTS } from '../../constants'; @@ -25,6 +30,8 @@ const EVENT_OPTIONS = Object.entries(SUBSCRIPTION_EVENTS).map( const ConnectionForm = ({ connection = null, isOpen, onClose }) => { const [submitting, setSubmitting] = useState(false); const [selectedEvents, setSelectedEvents] = useState([]); + const [headers, setHeaders] = useState([]); + const [payloadTemplates, setPayloadTemplates] = useState({}); const [apiError, setApiError] = useState(''); // One-time form @@ -71,9 +78,24 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { return acc; }, []) ); + // Initialize headers array from config.headers object + const cfgHeaders = connection.config?.headers || {}; + const hdrs = Object.keys(cfgHeaders).length + ? Object.entries(cfgHeaders).map(([k, v]) => ({ key: k, value: v })) + : [{ key: '', value: '' }]; + setHeaders(hdrs); + + // Initialize payload templates per subscription + const templates = {}; + connection.subscriptions.forEach((sub) => { + if (sub.payload_template) templates[sub.event] = sub.payload_template; + }); + setPayloadTemplates(templates); } else { form.reset(); setSelectedEvents([]); + setHeaders([{ key: '', value: '' }]); + setPayloadTemplates({}); } }, [connection]); @@ -87,10 +109,18 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { try { setSubmitting(true); setApiError(''); - const config = - values.type === 'webhook' - ? { url: values.url } - : { path: values.script_path }; + // Build config including optional headers + let config; + if (values.type === 'webhook') { + const hdrs = {}; + headers.forEach((h) => { + if (h.key && h.key.trim()) hdrs[h.key] = h.value; + }); + config = { url: values.url }; + if (Object.keys(hdrs).length) config.headers = hdrs; + } else { + config = { path: values.script_path }; + } if (connection) { await API.updateConnectIntegration(connection.id, { @@ -108,13 +138,14 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { }); } - await API.setConnectSubscriptions( - connection.id, - Object.keys(SUBSCRIPTION_EVENTS).map((event) => ({ - event, - enabled: selectedEvents.includes(event), - })) - ); + // Build subscription list including optional payload templates + const subs = Object.keys(SUBSCRIPTION_EVENTS).map((event) => ({ + event, + enabled: selectedEvents.includes(event), + payload_template: payloadTemplates[event] || null, + })); + + await API.setConnectSubscriptions(connection.id, subs); handleClose(); } catch (error) { console.error('Failed to create/update connection', error); @@ -164,64 +195,185 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { return (
- - {apiError ? ( - - {apiError} - - ) : null} - - + {form.getValues().type === 'webhook' ? ( + + ) : ( + + )} + + {form.getValues().type === 'webhook' ? ( + + + Custom Headers (optional) + + + {headers.map((h, idx) => ( + + { + const next = [...headers]; + next[idx] = { ...next[idx], key: e.target.value }; + setHeaders(next); + }} + style={{ flex: 1 }} + /> + { + const next = [...headers]; + next[idx] = { + ...next[idx], + value: e.target.value, + }; + setHeaders(next); + }} + style={{ flex: 1 }} + /> + + + ))} + + + + ) : null} - + - - - - + + + {EVENT_OPTIONS.map((opt) => ( + toggleEvent(opt.value)} + /> + ))} + + + + {form.getValues().type === 'webhook' && ( + + + + + Enable event triggers to set individual templates. + + +
+
+ + {EVENT_OPTIONS.map( + (opt) => + selectedEvents.includes(opt.value) && ( + + + {opt.label} + + +