mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: update webhook handling and triggering
This commit is contained in:
@ -2,16 +2,35 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
export type GetAllWebhooksByEventTriggerOptions = {
|
||||
eventTrigger: WebhookTriggerEvents;
|
||||
event: WebhookTriggerEvents;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const getAllWebhooksByEventTrigger = async ({
|
||||
eventTrigger,
|
||||
event,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetAllWebhooksByEventTriggerOptions) => {
|
||||
return prisma.webhook.findMany({
|
||||
where: {
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
eventTriggers: {
|
||||
has: eventTrigger,
|
||||
has: event,
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
58
packages/lib/server-only/webhooks/trigger/execute-webhook.ts
Normal file
58
packages/lib/server-only/webhooks/trigger/execute-webhook.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
Prisma,
|
||||
type Webhook,
|
||||
WebhookCallStatus,
|
||||
type WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
export type ExecuteWebhookOptions = {
|
||||
event: WebhookTriggerEvents;
|
||||
webhook: Webhook;
|
||||
data: unknown;
|
||||
};
|
||||
|
||||
export const executeWebhook = async ({ event, webhook, data }: ExecuteWebhookOptions) => {
|
||||
const { webhookUrl: url, secret } = webhook;
|
||||
|
||||
console.log('Executing webhook', { event, url });
|
||||
|
||||
const payload = {
|
||||
event,
|
||||
payload: data,
|
||||
createdAt: new Date().toISOString(),
|
||||
webhookEndpoint: url,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Documenso-Secret': secret ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
|
||||
|
||||
try {
|
||||
responseBody = JSON.parse(body);
|
||||
} catch (err) {
|
||||
responseBody = body;
|
||||
}
|
||||
|
||||
await prisma.webhookCall.create({
|
||||
data: {
|
||||
url,
|
||||
event,
|
||||
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
|
||||
requestBody: payload as Prisma.InputJsonValue,
|
||||
responseCode: response.status,
|
||||
responseBody,
|
||||
responseHeaders: Object.fromEntries(response.headers.entries()),
|
||||
webhookId: webhook.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
58
packages/lib/server-only/webhooks/trigger/handler.ts
Normal file
58
packages/lib/server-only/webhooks/trigger/handler.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { verify } from '../../crypto/verify';
|
||||
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
|
||||
import { executeWebhook } from './execute-webhook';
|
||||
import { ZTriggerWebhookBodySchema } from './schema';
|
||||
|
||||
export type HandlerTriggerWebhooksResponse =
|
||||
| {
|
||||
success: true;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export const handlerTriggerWebhooks = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<HandlerTriggerWebhooksResponse>,
|
||||
) => {
|
||||
const signature = req.headers['x-webhook-signature'];
|
||||
|
||||
if (typeof signature !== 'string') {
|
||||
console.log('Missing signature');
|
||||
return res.status(400).json({ success: false, error: 'Missing signature' });
|
||||
}
|
||||
|
||||
const valid = verify(req.body, signature);
|
||||
|
||||
if (!valid) {
|
||||
console.log('Invalid signature');
|
||||
return res.status(400).json({ success: false, error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
const result = ZTriggerWebhookBodySchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Invalid request body');
|
||||
return res.status(400).json({ success: false, error: 'Invalid request body' });
|
||||
}
|
||||
|
||||
const { event, data, userId, teamId } = result.data;
|
||||
|
||||
const allWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
|
||||
|
||||
await Promise.allSettled(
|
||||
allWebhooks.map(async (webhook) =>
|
||||
executeWebhook({
|
||||
event,
|
||||
webhook,
|
||||
data,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return res.status(200).json({ success: true, message: 'Webhooks executed successfully' });
|
||||
};
|
||||
12
packages/lib/server-only/webhooks/trigger/schema.ts
Normal file
12
packages/lib/server-only/webhooks/trigger/schema.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
export const ZTriggerWebhookBodySchema = z.object({
|
||||
event: z.nativeEnum(WebhookTriggerEvents),
|
||||
data: z.unknown(),
|
||||
userId: z.number(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TTriggerWebhookBodySchema = z.infer<typeof ZTriggerWebhookBodySchema>;
|
||||
40
packages/lib/server-only/webhooks/trigger/trigger-webhook.ts
Normal file
40
packages/lib/server-only/webhooks/trigger/trigger-webhook.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { sign } from '../../crypto/sign';
|
||||
|
||||
export type TriggerWebhookOptions = {
|
||||
event: WebhookTriggerEvents;
|
||||
data: Record<string, unknown>;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWebhookOptions) => {
|
||||
try {
|
||||
const body = {
|
||||
event,
|
||||
data,
|
||||
userId,
|
||||
teamId,
|
||||
};
|
||||
|
||||
const signature = sign(body);
|
||||
|
||||
await Promise.race([
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/webhook/trigger`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-webhook-signature': signature,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Request timeout')), 500);
|
||||
}),
|
||||
]).catch(() => null);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to trigger webhook`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user