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:
2026-03-28 13:54:54 +03:00
parent c41182ffd0
commit 6113a0039c
13 changed files with 459 additions and 5 deletions
+2
View File
@@ -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 ──
/**
+14 -1
View File
@@ -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,
};