mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
feat: test webhook functionality (#1886)
This commit is contained in:
170
apps/remix/app/components/dialogs/webhook-test-dialog.tsx
Normal file
170
apps/remix/app/components/dialogs/webhook-test-dialog.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Webhook } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type WebhookTestDialogProps = {
|
||||
webhook: Pick<Webhook, 'id' | 'webhookUrl' | 'eventTriggers'>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const ZTestWebhookFormSchema = z.object({
|
||||
event: z.nativeEnum(WebhookTriggerEvents),
|
||||
});
|
||||
|
||||
type TTestWebhookFormSchema = z.infer<typeof ZTestWebhookFormSchema>;
|
||||
|
||||
export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: testWebhook } = trpc.webhook.testWebhook.useMutation();
|
||||
|
||||
const form = useForm<TTestWebhookFormSchema>({
|
||||
resolver: zodResolver(ZTestWebhookFormSchema),
|
||||
defaultValues: {
|
||||
event: webhook.eventTriggers[0],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async ({ event }: TTestWebhookFormSchema) => {
|
||||
try {
|
||||
await testWebhook({
|
||||
id: webhook.id,
|
||||
event,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Test webhook sent`),
|
||||
description: _(msg`The test webhook has been successfully sent to your endpoint.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: _(msg`Test webhook failed`),
|
||||
description: _(
|
||||
msg`We encountered an error while sending the test webhook. Please check your endpoint and try again.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Test Webhook</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Send a test webhook with sample data to verify your integration is working correctly.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full flex-col space-y-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="event"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Event Type</Trans>
|
||||
</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an event type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{webhook.eventTriggers.map((event) => (
|
||||
<SelectItem key={event} value={event}>
|
||||
{toFriendlyWebhookEventName(event)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="rounded-md border p-4">
|
||||
<h4 className="mb-2 text-sm font-medium">
|
||||
<Trans>Webhook URL</Trans>
|
||||
</h4>
|
||||
<p className="text-muted-foreground break-all text-sm">{webhook.webhookUrl}</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Trans>Send Test Webhook</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -2,13 +2,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZEditWebhookRequestSchema } from '@documenso/trpc/server/webhook-router/schema';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
@ -21,9 +22,12 @@ import {
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
|
||||
import { Switch } from '@documenso/ui/primitives/switch';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { WebhookTestDialog } from '~/components/dialogs/webhook-test-dialog';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { WebhookMultiSelectCombobox } from '~/components/general/webhook-multiselect-combobox';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
@ -92,25 +96,45 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <SpinnerBox className="py-32" />;
|
||||
}
|
||||
|
||||
// Todo: Update UI, currently out of place.
|
||||
if (!webhook) {
|
||||
return (
|
||||
<GenericErrorLayout
|
||||
errorCode={404}
|
||||
errorCodeMap={{
|
||||
404: {
|
||||
heading: msg`Webhook not found`,
|
||||
subHeading: msg`404 Webhook not found`,
|
||||
message: msg`The webhook you are looking for may have been removed, renamed or may have never
|
||||
existed.`,
|
||||
},
|
||||
}}
|
||||
primaryButton={
|
||||
<Button asChild>
|
||||
<Link to={`/t/${team.url}/settings/webhooks`}>
|
||||
<Trans>Go back</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
secondaryButton={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-2xl">
|
||||
<SettingsHeader
|
||||
title={_(msg`Edit webhook`)}
|
||||
subtitle={_(msg`On this page, you can edit the webhook and its settings.`)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-white/50">
|
||||
<Loader className="h-8 w-8 animate-spin text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full max-w-xl flex-col gap-y-6"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@ -203,7 +227,7 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Trans>Update webhook</Trans>
|
||||
</Button>
|
||||
@ -211,6 +235,30 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<Alert
|
||||
className="mt-6 flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>
|
||||
<Trans>Test Webhook</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
<Trans>
|
||||
Send a test webhook with sample data to verify your integration is working correctly.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<WebhookTestDialog webhook={webhook}>
|
||||
<Button variant="outline" disabled={!webhook.enabled}>
|
||||
<Trans>Test Webhook</Trans>
|
||||
</Button>
|
||||
</WebhookTestDialog>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ export default function WebhookPage() {
|
||||
</div>
|
||||
)}
|
||||
{webhooks && webhooks.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
<div className="mt-4 flex max-w-2xl flex-col gap-y-4">
|
||||
{webhooks?.map((webhook) => (
|
||||
<div
|
||||
key={webhook.id}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type GetWebhookByIdOptions = {
|
||||
id: string;
|
||||
userId: number;
|
||||
@ -10,23 +13,11 @@ export const getWebhookById = async ({ id, userId, teamId }: GetWebhookByIdOptio
|
||||
return await prisma.webhook.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
team: {
|
||||
id: teamId,
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
44
packages/lib/server-only/webhooks/trigger-test-webhook.ts
Normal file
44
packages/lib/server-only/webhooks/trigger-test-webhook.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { getWebhookById } from './get-webhook-by-id';
|
||||
import { generateSampleWebhookPayload } from './trigger/generate-sample-data';
|
||||
import { triggerWebhook } from './trigger/trigger-webhook';
|
||||
|
||||
export type TriggerTestWebhookOptions = {
|
||||
id: string;
|
||||
event: WebhookTriggerEvents;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const triggerTestWebhook = async ({
|
||||
id,
|
||||
event,
|
||||
userId,
|
||||
teamId,
|
||||
}: TriggerTestWebhookOptions) => {
|
||||
const webhook = await getWebhookById({ id, userId, teamId });
|
||||
|
||||
if (!webhook.enabled) {
|
||||
throw new Error('Webhook is disabled');
|
||||
}
|
||||
|
||||
if (!webhook.eventTriggers.includes(event)) {
|
||||
throw new Error(`Webhook does not support event: ${event}`);
|
||||
}
|
||||
|
||||
const samplePayload = generateSampleWebhookPayload(event, webhook.webhookUrl);
|
||||
|
||||
try {
|
||||
await triggerWebhook({
|
||||
event,
|
||||
data: samplePayload,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return { success: true, message: 'Test webhook triggered successfully' };
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,485 @@
|
||||
import {
|
||||
DocumentDistributionMethod,
|
||||
DocumentSigningOrder,
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
DocumentVisibility,
|
||||
ReadStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import type { WebhookPayload } from '../../../types/webhook-payload';
|
||||
|
||||
export const generateSampleWebhookPayload = (
|
||||
event: WebhookTriggerEvents,
|
||||
webhookUrl: string,
|
||||
): WebhookPayload => {
|
||||
const now = new Date();
|
||||
const basePayload = {
|
||||
id: 10,
|
||||
externalId: null,
|
||||
userId: 1,
|
||||
authOptions: null,
|
||||
formValues: null,
|
||||
visibility: DocumentVisibility.EVERYONE,
|
||||
title: 'documenso.pdf',
|
||||
status: DocumentStatus.DRAFT,
|
||||
documentDataId: 'hs8qz1ktr9204jn7mg6c5dxy0',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
completedAt: null,
|
||||
deletedAt: null,
|
||||
teamId: null,
|
||||
templateId: null,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
id: 'doc_meta_123',
|
||||
subject: 'Please sign this document',
|
||||
message: 'Hello, please review and sign this document.',
|
||||
timezone: 'UTC',
|
||||
password: null,
|
||||
dateFormat: 'MM/DD/YYYY',
|
||||
redirectUrl: null,
|
||||
signingOrder: DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: false,
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
language: 'en',
|
||||
distributionMethod: DocumentDistributionMethod.EMAIL,
|
||||
emailSettings: null,
|
||||
},
|
||||
recipients: [
|
||||
{
|
||||
id: 52,
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer@documenso.com',
|
||||
name: 'John Doe',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.SIGNER,
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
id: 52,
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer@documenso.com',
|
||||
name: 'John Doe',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.SIGNER,
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_CREATED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.DRAFT,
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_SENT) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
email: 'signer2@documenso.com',
|
||||
name: 'Signer 2',
|
||||
role: RecipientRole.VIEWER,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.Recipient[0],
|
||||
email: 'signer1@documenso.com',
|
||||
name: 'Signer 1',
|
||||
token: 'SIGNING_TOKEN',
|
||||
signingOrder: 2,
|
||||
role: RecipientRole.SIGNER,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: null,
|
||||
rejectionReason: null,
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_OPENED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
email: 'signer2@documenso.com',
|
||||
name: 'Signer 2',
|
||||
role: RecipientRole.VIEWER,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.Recipient[0],
|
||||
email: 'signer2@documenso.com',
|
||||
name: 'Signer 2',
|
||||
role: RecipientRole.VIEWER,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_SIGNED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
completedAt: now,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
id: 51,
|
||||
email: 'signer1@documenso.com',
|
||||
name: 'Signer 1',
|
||||
token: 'SIGNING_TOKEN',
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.Recipient[0],
|
||||
id: 51,
|
||||
email: 'signer1@documenso.com',
|
||||
name: 'Signer 1',
|
||||
token: 'SIGNING_TOKEN',
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_COMPLETED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
completedAt: now,
|
||||
recipients: [
|
||||
{
|
||||
id: 50,
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer2@documenso.com',
|
||||
name: 'Signer 2',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.VIEWER,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer1@documenso.com',
|
||||
name: 'Signer 1',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
signingOrder: 2,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.SIGNER,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
id: 50,
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer2@documenso.com',
|
||||
name: 'Signer 2',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.VIEWER,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer1@documenso.com',
|
||||
name: 'Signer 1',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
signingOrder: 2,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.SIGNER,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_REJECTED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
status: DocumentStatus.PENDING,
|
||||
recipients: [
|
||||
{
|
||||
...basePayload.recipients[0],
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
rejectionReason: 'I do not agree with the terms',
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.REJECTED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signingOrder: 1,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
...basePayload.Recipient[0],
|
||||
signedAt: now,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
rejectionReason: 'I do not agree with the terms',
|
||||
readStatus: ReadStatus.OPENED,
|
||||
signingStatus: SigningStatus.REJECTED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signingOrder: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (event === WebhookTriggerEvents.DOCUMENT_CANCELLED) {
|
||||
return {
|
||||
event,
|
||||
payload: {
|
||||
...basePayload,
|
||||
id: 7,
|
||||
externalId: null,
|
||||
userId: 3,
|
||||
status: DocumentStatus.PENDING,
|
||||
documentDataId: 'cm6exvn93006hi02ru90a265a',
|
||||
documentMeta: {
|
||||
...basePayload.documentMeta,
|
||||
id: 'cm6exvn96006ji02rqvzjvwoy',
|
||||
subject: '',
|
||||
message: '',
|
||||
timezone: 'Etc/UTC',
|
||||
dateFormat: 'yyyy-MM-dd hh:mm a',
|
||||
redirectUrl: '',
|
||||
emailSettings: {
|
||||
documentDeleted: true,
|
||||
documentPending: true,
|
||||
recipientSigned: true,
|
||||
recipientRemoved: true,
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: true,
|
||||
recipientSigningRequest: true,
|
||||
},
|
||||
},
|
||||
recipients: [
|
||||
{
|
||||
id: 7,
|
||||
documentId: 7,
|
||||
templateId: null,
|
||||
email: 'signer1@documenso.com',
|
||||
name: 'Signer 1',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.SIGNER,
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
],
|
||||
Recipient: [
|
||||
{
|
||||
id: 7,
|
||||
documentId: 7,
|
||||
templateId: null,
|
||||
email: 'signer@documenso.com',
|
||||
name: 'Signer',
|
||||
token: 'SIGNING_TOKEN',
|
||||
documentDeletedAt: null,
|
||||
expired: null,
|
||||
signedAt: null,
|
||||
authOptions: {
|
||||
accessAuth: null,
|
||||
actionAuth: null,
|
||||
},
|
||||
signingOrder: 1,
|
||||
rejectionReason: null,
|
||||
role: RecipientRole.SIGNER,
|
||||
readStatus: ReadStatus.NOT_OPENED,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: now.toISOString(),
|
||||
webhookEndpoint: webhookUrl,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported event type: ${event}`);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Document, DocumentMeta, Recipient } from '@prisma/client';
|
||||
import type { Document, DocumentMeta, Recipient, WebhookTriggerEvents } from '@prisma/client';
|
||||
import {
|
||||
DocumentDistributionMethod,
|
||||
DocumentSigningOrder,
|
||||
@ -87,6 +87,13 @@ export const ZWebhookDocumentSchema = z.object({
|
||||
export type TWebhookRecipient = z.infer<typeof ZWebhookRecipientSchema>;
|
||||
export type TWebhookDocument = z.infer<typeof ZWebhookDocumentSchema>;
|
||||
|
||||
export type WebhookPayload = {
|
||||
event: WebhookTriggerEvents;
|
||||
payload: TWebhookDocument;
|
||||
createdAt: string;
|
||||
webhookEndpoint: string;
|
||||
};
|
||||
|
||||
export const mapDocumentToWebhookDocumentPayload = (
|
||||
document: Document & {
|
||||
recipients: Recipient[];
|
||||
|
||||
@ -3,6 +3,7 @@ import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-we
|
||||
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
|
||||
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
|
||||
import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
|
||||
import { triggerTestWebhook } from '@documenso/lib/server-only/webhooks/trigger-test-webhook';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import {
|
||||
@ -11,6 +12,7 @@ import {
|
||||
ZEditWebhookRequestSchema,
|
||||
ZGetTeamWebhooksRequestSchema,
|
||||
ZGetWebhookByIdRequestSchema,
|
||||
ZTriggerTestWebhookRequestSchema,
|
||||
} from './schema';
|
||||
|
||||
export const webhookRouter = router({
|
||||
@ -106,4 +108,25 @@ export const webhookRouter = router({
|
||||
teamId,
|
||||
});
|
||||
}),
|
||||
|
||||
testWebhook: authenticatedProcedure
|
||||
.input(ZTriggerTestWebhookRequestSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, event, teamId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
event,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return await triggerTestWebhook({
|
||||
id,
|
||||
event,
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@ -38,3 +38,11 @@ export const ZDeleteWebhookRequestSchema = z.object({
|
||||
});
|
||||
|
||||
export type TDeleteWebhookRequestSchema = z.infer<typeof ZDeleteWebhookRequestSchema>;
|
||||
|
||||
export const ZTriggerTestWebhookRequestSchema = z.object({
|
||||
id: z.string(),
|
||||
event: z.nativeEnum(WebhookTriggerEvents),
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export type TTriggerTestWebhookRequestSchema = z.infer<typeof ZTriggerTestWebhookRequestSchema>;
|
||||
|
||||
Reference in New Issue
Block a user