feat: generic webhook provider with JSONPath payload extraction

Add a new "webhook" provider type that accepts arbitrary HTTP POST payloads,
extracts template variables via user-defined JSONPath mappings, and dispatches
notifications through the existing pipeline. Supports three auth modes
(HMAC-SHA256, Bearer token, none), bounded JSONPath cache, and 1MB payload limit.

Full stack: core provider + event parser, API endpoint, DB migration,
capabilities, seeds, default templates (EN/RU), frontend descriptor, i18n.
This commit is contained in:
2026-03-27 23:51:14 +03:00
parent 307871cae5
commit 616b221c92
38 changed files with 603 additions and 0 deletions
+5
View File
@@ -115,6 +115,7 @@
"typeScheduler": "Scheduler",
"typeNut": "NUT (UPS)",
"typeGooglePhotos": "Google Photos",
"typeWebhook": "Generic Webhook",
"loadError": "Failed to load providers.",
"externalDomain": "External Domain",
"optional": "optional",
@@ -126,6 +127,9 @@
"plankaWebhookSecretHint": "Bearer token for webhook authentication. Set the same token as WEBHOOK_ACCESS_TOKEN in Planka.",
"plankaApiKeyHint": "Optional. Needed for connection testing and board listing.",
"plankaWebhookUrlHint": "Set this as the Webhook URL in Planka environment config (relative to your bridge host).",
"authMode": "Authentication Mode",
"authModeHint": "Choose hmac_sha256, bearer_token, or none",
"genericWebhookSecretHint": "Secret for HMAC-SHA256 or Bearer token authentication. Leave empty for no authentication.",
"webhookSecretRequired": "Webhook secret is required",
"apiToken": "API Token",
"apiTokenHint": "Optional. Needed for connection testing and repository listing.",
@@ -454,6 +458,7 @@
"upsReplaceBattery": "Replace battery",
"upsOverload": "UPS overloaded",
"scheduledMessage": "Scheduled message",
"webhookReceived": "Webhook received",
"trackImages": "Track images",
"trackVideos": "Track videos",
"favoritesOnly": "Favorites only",
+5
View File
@@ -115,6 +115,7 @@
"typeScheduler": "Планировщик",
"typeNut": "NUT (ИБП)",
"typeGooglePhotos": "Google Фото",
"typeWebhook": "Универсальный вебхук",
"loadError": "Не удалось загрузить провайдеры.",
"externalDomain": "Внешний домен",
"optional": "необязательно",
@@ -126,6 +127,9 @@
"plankaWebhookSecretHint": "Bearer-токен для аутентификации вебхуков. Укажите тот же токен как WEBHOOK_ACCESS_TOKEN в Planka.",
"plankaApiKeyHint": "Необязательно. Нужен для проверки подключения и получения списка досок.",
"plankaWebhookUrlHint": "Укажите этот URL в конфигурации Planka (относительно хоста bridge).",
"authMode": "Режим аутентификации",
"authModeHint": "Выберите hmac_sha256, bearer_token или none",
"genericWebhookSecretHint": "Секрет для HMAC-SHA256 или Bearer token аутентификации. Оставьте пустым для режима без аутентификации.",
"webhookSecretRequired": "Секрет вебхука обязателен",
"apiToken": "API токен",
"apiTokenHint": "Необязательно. Нужен для проверки подключения и получения списка репозиториев.",
@@ -454,6 +458,7 @@
"upsReplaceBattery": "Замена батареи",
"upsOverload": "Перегрузка ИБП",
"scheduledMessage": "Запланированное сообщение",
"webhookReceived": "Вебхук получен",
"trackImages": "Фото",
"trackVideos": "Видео",
"favoritesOnly": "Только избранные",
+2
View File
@@ -12,6 +12,7 @@ import { plankaDescriptor } from './planka';
import { schedulerDescriptor } from './scheduler';
import { nutDescriptor } from './nut';
import { googlePhotosDescriptor } from './google-photos';
import { webhookDescriptor } from './webhook';
const REGISTRY: ReadonlyMap<string, ProviderDescriptor> = new Map([
['immich', immichDescriptor],
@@ -20,6 +21,7 @@ const REGISTRY: ReadonlyMap<string, ProviderDescriptor> = new Map([
['scheduler', schedulerDescriptor],
['nut', nutDescriptor],
['google_photos', googlePhotosDescriptor],
['webhook', webhookDescriptor],
]);
/** Look up a provider descriptor by type. Returns null for unknown types. */
+49
View File
@@ -0,0 +1,49 @@
import type { ProviderDescriptor } from './types';
export const webhookDescriptor: ProviderDescriptor = {
type: 'webhook',
defaultName: 'Generic Webhook',
icon: 'mdiWebhook',
hasUrl: false,
configFields: [
{
key: 'auth_mode', configKey: 'auth_mode',
label: 'providers.authMode',
type: 'text',
placeholder: 'hmac_sha256 | bearer_token | none',
defaultValue: 'none',
hint: 'providers.authModeHint',
},
{
key: 'webhook_secret', configKey: 'webhook_secret',
label: 'providers.webhookSecret', editLabel: 'providers.webhookSecretKeep',
type: 'password', optional: true,
hint: 'providers.genericWebhookSecretHint',
},
],
buildConfig(form, editing) {
const config: Record<string, any> = {
auth_mode: form.auth_mode || 'none',
payload_mappings: form.payload_mappings || [],
};
if (form.webhook_secret) config.webhook_secret = form.webhook_secret;
if (form.event_type_path) config.event_type_path = form.event_type_path;
if (form.collection_path) config.collection_path = form.collection_path;
return { config };
},
hasConfigChanged(form, existing) {
return form.auth_mode !== (existing.auth_mode || 'none') ||
!!form.webhook_secret;
},
eventFields: [
{ key: 'track_webhook_received', label: 'trackingConfig.webhookReceived', default: true },
],
collectionMeta: null,
webhookBased: true,
webhookUrlPattern: '/api/webhooks/webhook/{id}',
};