feat: webhook payload history — store and display recent incoming payloads
Backend:
- WebhookPayloadLog model (provider_id, method, headers, body, status, extracted_fields, error_message)
- Auto-log payloads in generic_webhook() with matched/unmatched/error status
- Auto-prune beyond max_stored_payloads per provider
- Header filtering (only Content-Type, User-Agent, X-* stored; no Authorization)
- GET/DELETE /api/providers/{id}/webhook-logs endpoints
- store_payloads + max_stored_payloads in WebhookProviderConfig
Frontend:
- WebhookPayloadHistory.svelte — expandable log viewer with status badges, JSON body, headers, extracted fields
- payloadHistory flag on webhook provider descriptor
- max_stored_payloads config field (0 = disabled)
- Password confirmation field on change password modal
- i18n keys for webhook logs (en + ru)
This commit is contained in:
@@ -133,6 +133,8 @@
|
||||
"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.",
|
||||
"maxStoredPayloads": "Max stored payloads",
|
||||
"maxStoredPayloadsHint": "Number of recent payloads to keep for debugging (0 = disabled, max 100)",
|
||||
"webhookSecretRequired": "Webhook secret is required",
|
||||
"apiToken": "API Token",
|
||||
"apiTokenHint": "Optional. Needed for connection testing and repository listing.",
|
||||
@@ -152,11 +154,29 @@
|
||||
"gpRefreshTokenKeep": "Refresh Token (leave empty to keep current)",
|
||||
"gpRefreshTokenHint": "Obtain from Google OAuth Playground (developers.google.com/oauthplayground) with the Photos Library API scope.",
|
||||
"gpAllFieldsRequired": "Client ID, Client Secret, and Refresh Token are all required",
|
||||
"storePayloads": "Store incoming payloads",
|
||||
"storePayloadsHint": "Save recent webhook request bodies for debugging",
|
||||
"maxStoredPayloads": "Max stored payloads",
|
||||
"maxStoredPayloadsHint": "Number of recent payloads to keep (1-100)",
|
||||
"testAndSave": "Test & Save",
|
||||
"saveWithoutTest": "Save without testing",
|
||||
"selectType": "Select a provider type",
|
||||
"testFailed": "Connection test failed"
|
||||
},
|
||||
"webhookLogs": {
|
||||
"title": "Recent Payloads",
|
||||
"empty": "No payloads recorded yet",
|
||||
"clear": "Clear history",
|
||||
"confirmClear": "Clear all stored payloads for this provider?",
|
||||
"statusMatched": "Matched",
|
||||
"statusUnmatched": "Unmatched",
|
||||
"statusError": "Error",
|
||||
"headers": "Headers",
|
||||
"body": "Request Body",
|
||||
"extractedFields": "Extracted Fields",
|
||||
"errorMessage": "Error",
|
||||
"cleared": "Payload history cleared"
|
||||
},
|
||||
"notificationTracker": {
|
||||
"title": "Notification Trackers",
|
||||
"description": "Monitor albums for changes",
|
||||
@@ -951,6 +971,20 @@
|
||||
"providerGooglePhotos": "Google Photos albums & shared libraries",
|
||||
"providerWebhook": "Receive events via HTTP POST"
|
||||
},
|
||||
"webhookLogs": {
|
||||
"title": "Recent Payloads",
|
||||
"empty": "No payloads recorded yet",
|
||||
"clear": "Clear history",
|
||||
"confirmClear": "Clear all stored payloads for this provider?",
|
||||
"statusMatched": "Matched",
|
||||
"statusUnmatched": "Unmatched",
|
||||
"statusError": "Error",
|
||||
"headers": "Headers",
|
||||
"body": "Request Body",
|
||||
"extractedFields": "Extracted Fields",
|
||||
"errorMessage": "Error",
|
||||
"cleared": "Payload history cleared"
|
||||
},
|
||||
"error": {
|
||||
"notFound": "Page not found",
|
||||
"goHome": "Go home"
|
||||
|
||||
@@ -133,6 +133,8 @@
|
||||
"authMode": "Режим аутентификации",
|
||||
"authModeHint": "Выберите hmac_sha256, bearer_token или none",
|
||||
"genericWebhookSecretHint": "Секрет для HMAC-SHA256 или Bearer token аутентификации. Оставьте пустым для режима без аутентификации.",
|
||||
"maxStoredPayloads": "Макс. сохранённых запросов",
|
||||
"maxStoredPayloadsHint": "Количество сохраняемых запросов для отладки (0 = отключено, макс. 100)",
|
||||
"webhookSecretRequired": "Секрет вебхука обязателен",
|
||||
"apiToken": "API токен",
|
||||
"apiTokenHint": "Необязательно. Нужен для проверки подключения и получения списка репозиториев.",
|
||||
@@ -152,11 +154,29 @@
|
||||
"gpRefreshTokenKeep": "Refresh Token (оставьте пустым для сохранения текущего)",
|
||||
"gpRefreshTokenHint": "Получите через Google OAuth Playground (developers.google.com/oauthplayground) с областью Photos Library API.",
|
||||
"gpAllFieldsRequired": "Client ID, Client Secret и Refresh Token обязательны",
|
||||
"storePayloads": "Сохранять входящие данные",
|
||||
"storePayloadsHint": "Сохранять тела недавних вебхук-запросов для отладки",
|
||||
"maxStoredPayloads": "Макс. сохранённых запросов",
|
||||
"maxStoredPayloadsHint": "Количество сохраняемых запросов (1-100)",
|
||||
"testAndSave": "Проверить и сохранить",
|
||||
"saveWithoutTest": "Сохранить без проверки",
|
||||
"selectType": "Выберите тип провайдера",
|
||||
"testFailed": "Ошибка проверки подключения"
|
||||
},
|
||||
"webhookLogs": {
|
||||
"title": "Последние запросы",
|
||||
"empty": "Записей пока нет",
|
||||
"clear": "Очистить историю",
|
||||
"confirmClear": "Очистить все сохранённые запросы для этого провайдера?",
|
||||
"statusMatched": "Совпадение",
|
||||
"statusUnmatched": "Не совпало",
|
||||
"statusError": "Ошибка",
|
||||
"headers": "Заголовки",
|
||||
"body": "Тело запроса",
|
||||
"extractedFields": "Извлечённые поля",
|
||||
"errorMessage": "Ошибка",
|
||||
"cleared": "История запросов очищена"
|
||||
},
|
||||
"notificationTracker": {
|
||||
"title": "Трекеры уведомлений",
|
||||
"description": "Отслеживание изменений в альбомах",
|
||||
@@ -951,6 +971,20 @@
|
||||
"providerGooglePhotos": "Альбомы и общие библиотеки Google Фото",
|
||||
"providerWebhook": "Приём событий через HTTP POST"
|
||||
},
|
||||
"webhookLogs": {
|
||||
"title": "Последние запросы",
|
||||
"empty": "Записей пока нет",
|
||||
"clear": "Очистить историю",
|
||||
"confirmClear": "Очистить все сохранённые запросы для этого провайдера?",
|
||||
"statusMatched": "Совпадение",
|
||||
"statusUnmatched": "Не совпало",
|
||||
"statusError": "Ошибка",
|
||||
"headers": "Заголовки",
|
||||
"body": "Тело запроса",
|
||||
"extractedFields": "Извлечённые поля",
|
||||
"errorMessage": "Ошибка",
|
||||
"cleared": "История запросов очищена"
|
||||
},
|
||||
"error": {
|
||||
"notFound": "Страница не найдена",
|
||||
"goHome": "На главную"
|
||||
|
||||
@@ -139,6 +139,8 @@ export interface ProviderDescriptor {
|
||||
// ── Webhook URL display ──
|
||||
/** Pattern shown in edit mode, e.g. "/api/webhooks/gitea/{id}". */
|
||||
webhookUrlPattern?: string;
|
||||
/** Whether this provider stores incoming payload history for debugging. */
|
||||
payloadHistory?: boolean;
|
||||
|
||||
// ── Provider-specific hooks ──
|
||||
/**
|
||||
|
||||
@@ -21,12 +21,22 @@ export const webhookDescriptor: ProviderDescriptor = {
|
||||
type: 'password', optional: true,
|
||||
hint: 'providers.genericWebhookSecretHint',
|
||||
},
|
||||
{
|
||||
key: 'max_stored_payloads', configKey: 'max_stored_payloads',
|
||||
label: 'providers.maxStoredPayloads',
|
||||
type: 'number',
|
||||
min: 0, max: 100,
|
||||
defaultValue: 20,
|
||||
hint: 'providers.maxStoredPayloadsHint',
|
||||
},
|
||||
],
|
||||
|
||||
buildConfig(form, editing) {
|
||||
const config: Record<string, any> = {
|
||||
auth_mode: form.auth_mode || 'none',
|
||||
payload_mappings: form.payload_mappings || [],
|
||||
store_payloads: form.store_payloads !== '0' && form.store_payloads !== 0,
|
||||
max_stored_payloads: Math.max(1, Math.min(100, Number(form.max_stored_payloads) || 20)),
|
||||
};
|
||||
if (form.webhook_secret) config.webhook_secret = form.webhook_secret;
|
||||
if (form.event_type_path) config.event_type_path = form.event_type_path;
|
||||
@@ -36,7 +46,9 @@ export const webhookDescriptor: ProviderDescriptor = {
|
||||
|
||||
hasConfigChanged(form, existing) {
|
||||
return form.auth_mode !== (existing.auth_mode || 'none') ||
|
||||
!!form.webhook_secret;
|
||||
!!form.webhook_secret ||
|
||||
(form.store_payloads !== '0' && form.store_payloads !== 0) !== (existing.store_payloads !== false) ||
|
||||
Number(form.max_stored_payloads || 20) !== Number(existing.max_stored_payloads || 20);
|
||||
},
|
||||
|
||||
eventFields: [
|
||||
@@ -46,4 +58,5 @@ export const webhookDescriptor: ProviderDescriptor = {
|
||||
collectionMeta: null,
|
||||
webhookBased: true,
|
||||
webhookUrlPattern: '/api/webhooks/webhook/{id}',
|
||||
payloadHistory: true,
|
||||
};
|
||||
|
||||
@@ -181,6 +181,18 @@ export interface EventLog {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface WebhookPayloadLog {
|
||||
id: number;
|
||||
provider_id: number;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
body: Record<string, any>;
|
||||
status: 'matched' | 'unmatched' | 'error';
|
||||
extracted_fields: Record<string, any>;
|
||||
error_message: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
|
||||
Reference in New Issue
Block a user