feat(webhook): add support for custom headers in webhook notifications

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
This commit is contained in:
0xsysr3ll
2025-12-14 22:40:50 +01:00
parent 3ee69663dc
commit 37b83fe56d
5 changed files with 135 additions and 8 deletions

View File

@@ -196,16 +196,33 @@ class WebhookAgent
}
try {
const headers: Record<string, string> = {};
if (settings.options.authHeader) {
headers.Authorization = settings.options.authHeader;
}
if (
settings.options.customHeaders &&
settings.options.customHeaders.length > 0
) {
settings.options.customHeaders.forEach((header) => {
if (header.key && header.value) {
// Don't override Authorization header if it's already set via authHeader
if (
header.key.toLowerCase() !== 'authorization' ||
!settings.options.authHeader
) {
headers[header.key] = header.value;
}
}
});
}
await axios.post(
webhookUrl,
this.buildPayload(type, payload),
settings.options.authHeader
? {
headers: {
Authorization: settings.options.authHeader,
},
}
: undefined
Object.keys(headers).length > 0 ? { headers } : undefined
);
return true;

View File

@@ -275,6 +275,7 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
webhookUrl: string;
jsonPayload: string;
authHeader?: string;
customHeaders?: { key: string; value: string }[];
supportVariables?: boolean;
};
}

View File

@@ -279,6 +279,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
'utf8'
)
),
customHeaders: webhookSettings.options.customHeaders ?? [],
supportVariables: webhookSettings.options.supportVariables ?? false,
},
};
@@ -301,6 +302,7 @@ notificationRoutes.post('/webhook', async (req, res, next) => {
),
webhookUrl: req.body.options.webhookUrl,
authHeader: req.body.options.authHeader,
customHeaders: req.body.options.customHeaders ?? [],
supportVariables: req.body.options.supportVariables ?? false,
},
};
@@ -333,6 +335,7 @@ notificationRoutes.post('/webhook/test', async (req, res, next) => {
),
webhookUrl: req.body.options.webhookUrl,
authHeader: req.body.options.authHeader,
customHeaders: req.body.options.customHeaders ?? [],
supportVariables: req.body.options.supportVariables ?? false,
},
};

View File

@@ -5,7 +5,12 @@ import SettingsBadge from '@app/components/Settings/SettingsBadge';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { isValidURL } from '@app/utils/urlValidationHelper';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import {
ArrowDownOnSquareIcon,
BeakerIcon,
PlusIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
import {
ArrowPathIcon,
QuestionMarkCircleIcon,
@@ -80,6 +85,15 @@ const messages = defineMessages(
supportVariablesTip:
'Available variables are documented in the webhook template variables section',
authheader: 'Authorization Header',
customHeaders: 'Custom Headers',
customHeadersTip:
'Add custom HTTP headers to include with webhook requests',
customHeadersAdd: 'Add Header',
customHeadersRemove: 'Remove',
customHeadersKey: 'Header Name',
customHeadersValue: 'Header Value',
customHeadersKeyRequired: 'Header name is required',
customHeadersValueRequired: 'Header value is required',
validationJsonPayloadRequired: 'You must provide a valid JSON payload',
webhooksettingssaved: 'Webhook notification settings saved successfully!',
webhooksettingsfailed: 'Webhook notification settings failed to save.',
@@ -125,6 +139,8 @@ const NotificationsWebhook = () => {
supportVariables: Yup.boolean(),
customHeaders: Yup.array(),
jsonPayload: Yup.string()
.when('enabled', {
is: true,
@@ -159,6 +175,7 @@ const NotificationsWebhook = () => {
webhookUrl: data.options.webhookUrl,
jsonPayload: data.options.jsonPayload,
authHeader: data.options.authHeader,
customHeaders: data.options.customHeaders ?? [],
supportVariables: data.options.supportVariables ?? false,
}}
validationSchema={NotificationsWebhookSchema}
@@ -171,6 +188,9 @@ const NotificationsWebhook = () => {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
customHeaders: values.customHeaders.filter(
(h: { key: string; value: string }) => h.key && h.value
),
supportVariables: values.supportVariables,
},
});
@@ -229,6 +249,9 @@ const NotificationsWebhook = () => {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
customHeaders: values.customHeaders.filter(
(h: { key: string; value: string }) => h.key && h.value
),
supportVariables: values.supportVariables ?? false,
},
});
@@ -344,6 +367,81 @@ const NotificationsWebhook = () => {
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="customHeaders" className="text-label">
{intl.formatMessage(messages.customHeaders)}
<span className="label-tip">
{intl.formatMessage(messages.customHeadersTip)}
</span>
</label>
<div className="form-input-area">
<div className="space-y-2">
{values.customHeaders.map(
(header: { key: string; value: string }, index: number) => (
<div key={index} className="flex gap-2">
<div className="flex-1">
<div className="form-input-field">
<Field
name={`customHeaders.${index}.key`}
type="text"
placeholder={intl.formatMessage(
messages.customHeadersKey
)}
/>
</div>
</div>
<div className="flex-1">
<div className="form-input-field">
<Field
name={`customHeaders.${index}.value`}
type="text"
placeholder={intl.formatMessage(
messages.customHeadersValue
)}
/>
</div>
</div>
<div className="flex items-center">
<Button
buttonType="danger"
buttonSize="sm"
onClick={(e) => {
e.preventDefault();
const newHeaders = values.customHeaders.filter(
(
_: { key: string; value: string },
i: number
) => i !== index
);
setFieldValue('customHeaders', newHeaders);
}}
title={intl.formatMessage(
messages.customHeadersRemove
)}
>
<TrashIcon />
</Button>
</div>
</div>
)
)}
<Button
buttonType="default"
buttonSize="sm"
onClick={(e) => {
e.preventDefault();
setFieldValue('customHeaders', [
...values.customHeaders,
{ key: '', value: '' },
]);
}}
>
<PlusIcon />
<span>{intl.formatMessage(messages.customHeadersAdd)}</span>
</Button>
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="webhook-json-payload" className="text-label">
{intl.formatMessage(messages.customJson)}

View File

@@ -681,6 +681,14 @@
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsWebhook.authheader": "Authorization Header",
"components.Settings.Notifications.NotificationsWebhook.customHeaders": "Custom Headers",
"components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "Add Header",
"components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Header Name",
"components.Settings.Notifications.NotificationsWebhook.customHeadersKeyRequired": "Header name is required",
"components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "Remove",
"components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "Add custom HTTP headers to include with webhook requests",
"components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Header Value",
"components.Settings.Notifications.NotificationsWebhook.customHeadersValueRequired": "Header value is required",
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default",
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!",