mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 12:32:34 +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 { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Loader } from 'lucide-react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
import { Link } from 'react-router';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZEditWebhookRequestSchema } from '@documenso/trpc/server/webhook-router/schema';
|
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 { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -21,9 +22,12 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||||
|
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
|
||||||
import { Switch } from '@documenso/ui/primitives/switch';
|
import { Switch } from '@documenso/ui/primitives/switch';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
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 { SettingsHeader } from '~/components/general/settings-header';
|
||||||
import { WebhookMultiSelectCombobox } from '~/components/general/webhook-multiselect-combobox';
|
import { WebhookMultiSelectCombobox } from '~/components/general/webhook-multiselect-combobox';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
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 (
|
return (
|
||||||
<div>
|
<div className="max-w-2xl">
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title={_(msg`Edit webhook`)}
|
title={_(msg`Edit webhook`)}
|
||||||
subtitle={_(msg`On this page, you can edit the webhook and its settings.`)}
|
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 {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<fieldset
|
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||||
className="flex h-full max-w-xl flex-col gap-y-6"
|
|
||||||
disabled={form.formState.isSubmitting}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
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}>
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
<Trans>Update webhook</Trans>
|
<Trans>Update webhook</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -211,6 +235,30 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export default function WebhookPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{webhooks && webhooks.length > 0 && (
|
{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) => (
|
{webhooks?.map((webhook) => (
|
||||||
<div
|
<div
|
||||||
key={webhook.id}
|
key={webhook.id}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||||
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
|
|
||||||
export type GetWebhookByIdOptions = {
|
export type GetWebhookByIdOptions = {
|
||||||
id: string;
|
id: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -10,23 +13,11 @@ export const getWebhookById = async ({ id, userId, teamId }: GetWebhookByIdOptio
|
|||||||
return await prisma.webhook.findFirstOrThrow({
|
return await prisma.webhook.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
userId,
|
team: buildTeamWhereQuery({
|
||||||
team: {
|
teamId,
|
||||||
id: teamId,
|
userId,
|
||||||
teamGroups: {
|
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
|
||||||
some: {
|
}),
|
||||||
organisationGroup: {
|
|
||||||
organisationGroupMembers: {
|
|
||||||
some: {
|
|
||||||
organisationMember: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
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 {
|
import {
|
||||||
DocumentDistributionMethod,
|
DocumentDistributionMethod,
|
||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
@ -87,6 +87,13 @@ export const ZWebhookDocumentSchema = z.object({
|
|||||||
export type TWebhookRecipient = z.infer<typeof ZWebhookRecipientSchema>;
|
export type TWebhookRecipient = z.infer<typeof ZWebhookRecipientSchema>;
|
||||||
export type TWebhookDocument = z.infer<typeof ZWebhookDocumentSchema>;
|
export type TWebhookDocument = z.infer<typeof ZWebhookDocumentSchema>;
|
||||||
|
|
||||||
|
export type WebhookPayload = {
|
||||||
|
event: WebhookTriggerEvents;
|
||||||
|
payload: TWebhookDocument;
|
||||||
|
createdAt: string;
|
||||||
|
webhookEndpoint: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const mapDocumentToWebhookDocumentPayload = (
|
export const mapDocumentToWebhookDocumentPayload = (
|
||||||
document: Document & {
|
document: Document & {
|
||||||
recipients: Recipient[];
|
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 { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
|
||||||
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
|
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 { 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 { authenticatedProcedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
ZEditWebhookRequestSchema,
|
ZEditWebhookRequestSchema,
|
||||||
ZGetTeamWebhooksRequestSchema,
|
ZGetTeamWebhooksRequestSchema,
|
||||||
ZGetWebhookByIdRequestSchema,
|
ZGetWebhookByIdRequestSchema,
|
||||||
|
ZTriggerTestWebhookRequestSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
export const webhookRouter = router({
|
export const webhookRouter = router({
|
||||||
@ -106,4 +108,25 @@ export const webhookRouter = router({
|
|||||||
teamId,
|
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 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