mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: multiple-webhook-headers and nextcloud talk provider (#296)
* feat(webhook): replace single secret header pair with webhookHeaders array schema * feat(webhook): iterate webhookHeaders array; keep backward compat for legacy secret fields * feat(webhook): replace single header pair with dynamic useFieldArray header list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(webhook): add field labels to header key/value inputs * fix(webhook): guard reserved headers; align delete button with inputs * feat(nextcloud): add nextcloud to provider_kind enum * feat(nextcloud): add ProviderKind entry and HMAC-signed provider * feat(nextcloud): add Zod schema and form component * feat(nextcloud): wire nextcloud into form registry and UI provider list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(nextcloud): register sendNextcloud in provider dispatch map * fix: multiple webhook headers and nextcloud talk notification provider * test(webhook): update e2e to use Add Header button and Header Value label * fix: webhook.ts --------- Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
charles-gauthereau
parent
2cd5bda416
commit
af3df7f72a
@@ -7,6 +7,7 @@ import {sendTelegram} from "@/features/notifications/providers/telegram";
|
||||
import {sendGotify} from "@/features/notifications/providers/gotify";
|
||||
import {sendNtfy} from "@/features/notifications/providers/ntfy";
|
||||
import {sendWebhook} from "@/features/notifications/providers/webhook";
|
||||
import {sendNextcloud} from "@/features/notifications/providers/nextcloud";
|
||||
|
||||
const handlers: Record<
|
||||
ProviderKind,
|
||||
@@ -18,7 +19,8 @@ const handlers: Record<
|
||||
telegram: sendTelegram,
|
||||
gotify: sendGotify,
|
||||
ntfy: sendNtfy,
|
||||
webhook: sendWebhook
|
||||
webhook: sendWebhook,
|
||||
nextcloud: sendNextcloud,
|
||||
};
|
||||
|
||||
export async function dispatchViaProvider(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createHmac, randomBytes } from "crypto";
|
||||
import type { EventPayload, DispatchResult } from "../types";
|
||||
|
||||
type NextcloudConfig = {
|
||||
nextcloudUrl: string;
|
||||
nextcloudBotToken: string;
|
||||
nextcloudBotSecret: string;
|
||||
};
|
||||
|
||||
function formatPayloadData(data: unknown): string {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendNextcloud(
|
||||
config: NextcloudConfig,
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {
|
||||
nextcloudUrl,
|
||||
nextcloudBotToken,
|
||||
nextcloudBotSecret,
|
||||
} = config;
|
||||
|
||||
const payloadData = formatPayloadData(payload.data);
|
||||
|
||||
const messageParts = [
|
||||
`[${payload.level.toUpperCase()}] ${payload.title}`,
|
||||
payload.message,
|
||||
];
|
||||
|
||||
if (payloadData) {
|
||||
messageParts.push(`Payload:\n${payloadData}`);
|
||||
}
|
||||
|
||||
const message = messageParts.join("\n\n");
|
||||
|
||||
const random = randomBytes(32).toString("hex");
|
||||
|
||||
const signature = createHmac("sha256", nextcloudBotSecret)
|
||||
.update(random + message)
|
||||
.digest("hex");
|
||||
|
||||
const baseUrl = nextcloudUrl.replace(/\/$/, "");
|
||||
|
||||
const res = await fetch(
|
||||
`${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${nextcloudBotToken}/message`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"OCS-APIRequest": "true",
|
||||
"X-Nextcloud-Talk-Bot-Random": random,
|
||||
"X-Nextcloud-Talk-Bot-Signature": signature,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
|
||||
throw new Error(
|
||||
`Nextcloud error: ${res.status} ${err}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "nextcloud",
|
||||
message: "Sent to Nextcloud Talk",
|
||||
response: await res.text(),
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,38 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
import type { EventPayload, DispatchResult } from '../types';
|
||||
|
||||
type WebhookConfig = {
|
||||
webhookUrl: string;
|
||||
webhookHeaders?: { key: string; value: string }[];
|
||||
webhookSecret?: string;
|
||||
webhookSecretHeader?: string;
|
||||
};
|
||||
|
||||
export async function sendWebhook(
|
||||
config: { webhookUrl: string; webhookSecret?: string; webhookSecretHeader?: string },
|
||||
config: WebhookConfig,
|
||||
payload: EventPayload
|
||||
): Promise<DispatchResult> {
|
||||
const {webhookUrl, webhookSecret, webhookSecretHeader} = config;
|
||||
const { webhookUrl, webhookHeaders, webhookSecret, webhookSecretHeader } = config;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Portabase-Notifier/1.0'
|
||||
'User-Agent': 'Portabase-Notifier/1.0',
|
||||
};
|
||||
|
||||
if (webhookSecret) {
|
||||
// New format: iterate custom headers array
|
||||
if (webhookHeaders && webhookHeaders.length > 0) {
|
||||
const RESERVED = new Set(['content-type', 'user-agent']);
|
||||
for (const { key, value } of webhookHeaders) {
|
||||
if (key && !RESERVED.has(key.toLowerCase())) headers[key] = value;
|
||||
}
|
||||
} else if (webhookSecret) {
|
||||
// Legacy format: single secret header pair
|
||||
headers[webhookSecretHeader || 'X-Webhook-Secret'] = webhookSecret;
|
||||
}
|
||||
|
||||
const res = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: headers,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook';
|
||||
export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook' | 'nextcloud';
|
||||
|
||||
export interface DispatchResult {
|
||||
success: boolean;
|
||||
|
||||
Reference in New Issue
Block a user