Compare commits

..

6 Commits

31 changed files with 597 additions and 1348 deletions

View File

@ -22,15 +22,6 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
- `document.completed`
- `document.rejected`
- `document.cancelled`
- `document.viewed`
- `document.recipient.completed`
- `document.downloaded`
- `document.reminder.sent`
- `template.created`
- `template.updated`
- `template.deleted`
- `template.used`
- `recipient.authentication.failed`
## Create a webhook subscription
@ -47,7 +38,7 @@ Clicking on the "**Create Webhook**" button opens a modal to create a new webhoo
To create a new webhook subscription, you need to provide the following information:
- Enter the webhook URL that will receive the event payload.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`, `document.viewed`, `document.recipient.completed`, `document.downloaded`, `document.reminder.sent`, `template.created`, `template.updated`, `template.deleted`, `template.used`, `recipient.authentication.failed`.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`.
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
![A screenshot of the Create Webhook modal that shows the URL input field and the event checkboxes](/webhook-images/webhooks-page-create-webhook-modal.webp)
@ -628,591 +619,6 @@ Example payload for the `document.rejected` event:
}
```
Example payload for the `document.viewed` event:
```json
{
"event": "DOCUMENT_VIEWED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "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": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "John Doe",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:50:26.174Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `document.recipient.completed` event:
```json
{
"event": "DOCUMENT_RECIPIENT_COMPLETED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:51:10.055Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "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": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 50,
"documentId": 10,
"templateId": null,
"email": "signer1@documenso.com",
"name": "Signer 1",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:51:10.055Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
},
{
"id": 51,
"documentId": 10,
"templateId": null,
"email": "signer2@documenso.com",
"name": "Signer 2",
"token": "HkrptwS42ZBXdRKj1TyUo",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 2,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:51:10.577Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `document.downloaded` event:
```json
{
"event": "DOCUMENT_DOWNLOADED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "COMPLETED",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:52:05.708Z",
"completedAt": "2024-04-22T11:52:05.707Z",
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "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": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 51,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "HkrptwS42ZBXdRKj1TyUo",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:52:05.688Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:53:18.577Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `document.reminder.sent` event:
```json
{
"event": "DOCUMENT_REMINDER_SENT",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "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": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T12:00:00.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.created` event:
```json
{
"event": "TEMPLATE_CREATED",
"payload": {
"id": 5,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:44:43.341Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_456",
"subject": "Employment Contract",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 25,
"documentId": null,
"templateId": 5,
"email": "employee@company.com",
"name": "Employee",
"token": "TemplateToken123",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T11:44:44.779Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.updated` event:
```json
{
"event": "TEMPLATE_UPDATED",
"payload": {
"id": 5,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract_v2.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T12:30:00.000Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_456",
"subject": "Employment Contract - Updated",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 25,
"documentId": null,
"templateId": 5,
"email": "employee@company.com",
"name": "Employee",
"token": "TemplateToken123",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T12:30:01.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.deleted` event:
```json
{
"event": "TEMPLATE_DELETED",
"payload": {
"id": 5,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:44:43.341Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_456",
"subject": "Employment Contract",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 25,
"documentId": null,
"templateId": 5,
"email": "employee@company.com",
"name": "Employee",
"token": "TemplateToken123",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T13:00:00.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.used` event:
```json
{
"event": "TEMPLATE_USED",
"payload": {
"id": 15,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract.pdf",
"status": "DRAFT",
"documentDataId": "new_doc_data_123",
"createdAt": "2024-04-22T14:00:00.000Z",
"updatedAt": "2024-04-22T14:00:00.000Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_789",
"subject": "Employment Contract",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 60,
"documentId": 15,
"templateId": 5,
"email": "newemployee@company.com",
"name": "New Employee",
"token": "DocToken456",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T14:00:01.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `recipient.authentication.failed` event:
```json
{
"event": "RECIPIENT_AUTHENTICATION_FAILED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "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": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": {
"accessAuth": "TWO_FACTOR_AUTH",
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:49:00.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
## Webhook Events Testing
You can trigger test webhook events to test the webhook functionality. To trigger a test webhook, navigate to the [Webhooks page](/developers/webhooks) and click on the "Test Webhook" button.

View File

@ -89,10 +89,7 @@ export const DirectTemplatePageView = ({
setStep('sign');
};
const onSignDirectTemplateSubmit = async (
fields: DirectTemplateLocalField[],
nextSigner?: { name: string; email: string },
) => {
const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
try {
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
@ -101,7 +98,6 @@ export const DirectTemplatePageView = ({
}
const { token } = await createDocumentFromDirectTemplate({
nextSigner,
directTemplateToken,
directTemplateExternalId,
directRecipientName: fullName,

View File

@ -55,13 +55,10 @@ import { DocumentSigningRecipientProvider } from '../document-signing/document-s
export type DirectTemplateSigningFormProps = {
flowStep: DocumentFlowStep;
directRecipient: Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id'>;
directRecipient: Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
directRecipientFields: Field[];
template: Omit<TTemplate, 'user'>;
onSubmit: (
_data: DirectTemplateLocalField[],
_nextSigner?: { name: string; email: string },
) => Promise<void>;
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
};
export type DirectTemplateLocalField = Field & {
@ -152,7 +149,7 @@ export const DirectTemplateSigningForm = ({
validateFieldsInserted(fieldsRequiringValidation);
};
const handleSubmit = async (nextSigner?: { name: string; email: string }) => {
const handleSubmit = async () => {
setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
@ -164,7 +161,7 @@ export const DirectTemplateSigningForm = ({
setIsSubmitting(true);
try {
await onSubmit(localFields, nextSigner);
await onSubmit(localFields);
} catch {
setIsSubmitting(false);
}
@ -221,30 +218,6 @@ export const DirectTemplateSigningForm = ({
setLocalFields(updatedFields);
}, []);
const nextRecipient = useMemo(() => {
if (
!template.templateMeta?.signingOrder ||
template.templateMeta.signingOrder !== 'SEQUENTIAL' ||
!template.templateMeta.allowDictateNextSigner
) {
return undefined;
}
const sortedRecipients = template.recipients.sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
if (a.signingOrder === null) return 1;
if (b.signingOrder === null) return -1;
if (a.signingOrder === b.signingOrder) return a.id - b.id;
return a.signingOrder - b.signingOrder;
});
const currentIndex = sortedRecipients.findIndex((r) => r.id === directRecipient.id);
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
? sortedRecipients[currentIndex + 1]
: undefined;
}, [template.templateMeta?.signingOrder, template.recipients, directRecipient.id]);
return (
<DocumentSigningRecipientProvider recipient={directRecipient}>
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
@ -444,15 +417,11 @@ export const DirectTemplateSigningForm = ({
<DocumentSigningCompleteDialog
isSubmitting={isSubmitting}
onSignatureComplete={async (nextSigner) => handleSubmit(nextSigner)}
onSignatureComplete={async () => handleSubmit()}
documentTitle={template.title}
fields={localFields}
fieldsValidated={fieldsValidated}
recipient={directRecipient}
allowDictateNextSigner={nextRecipient && template.templateMeta?.allowDictateNextSigner}
defaultNextSigner={
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
}
/>
</div>
</DocumentFlowFormContainerFooter>

View File

@ -9,7 +9,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentSigningAuthPageViewProps = {
email?: string;
email: string;
emailHasAccount?: boolean;
};
@ -22,18 +22,12 @@ export const DocumentSigningAuthPageView = ({
const [isSigningOut, setIsSigningOut] = useState(false);
const handleChangeAccount = async (email?: string) => {
const handleChangeAccount = async (email: string) => {
try {
setIsSigningOut(true);
let redirectPath = '/signin';
if (email) {
redirectPath = emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`;
}
await authClient.signOut({
redirectPath,
redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
});
} catch {
toast({
@ -55,13 +49,9 @@ export const DocumentSigningAuthPageView = ({
</h1>
<p className="text-muted-foreground mt-2 text-sm">
{email ? (
<Trans>
You need to be logged in as <strong>{email}</strong> to view this page.
</Trans>
) : (
<Trans>You need to be logged in to view this page.</Trans>
)}
<Trans>
You need to be logged in as <strong>{email}</strong> to view this page.
</Trans>
</p>
<Button

View File

@ -24,10 +24,7 @@ type PasskeyData = {
isError: boolean;
};
type SigningAuthRecipient = Pick<
Recipient,
'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id'
>;
type SigningAuthRecipient = Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
export type DocumentSigningAuthContextValue = {
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;

View File

@ -304,6 +304,7 @@ export const DocumentSigningCompleteDialog = ({
<form onSubmit={form.handleSubmit(onFormSubmit)}>
{allowDictateNextSigner && defaultNextSigner && (
<div className="mb-4 flex flex-col gap-4">
{/* Todo: Envelopes - Should we say "The next recipient to sign this document will be"? */}
<div className="flex flex-col gap-4 md:flex-row">
<FormField
control={form.control}

View File

@ -285,6 +285,8 @@ export const EnvelopeSigningProvider = ({
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => {
console.log('insertField', fieldId, fieldValue);
// Set the field locally for direct templates.
if (isDirectTemplate) {
handleDirectTemplateFieldInsertion(fieldId, fieldValue);

View File

@ -127,7 +127,6 @@ export const EnvelopeSignerCompleteDialog = () => {
isBase64,
};
}),
nextSigner,
});
const redirectUrl = envelope.documentMeta.redirectUrl;

View File

@ -8,6 +8,7 @@ import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/env
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-direct-template-signing';
import { getEnvelopeRequiredAccessData } from '@documenso/lib/server-only/envelope/get-envelope-required-access-data';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@ -97,12 +98,15 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
envelopeForSigning,
} as const;
})
.catch((e) => {
.catch(async (e) => {
const error = AppError.parseError(e);
if (error.code === AppErrorCode.UNAUTHORIZED) {
const requiredAccessData = await getEnvelopeRequiredAccessData({ token });
return {
isDocumentAccessValid: false,
...requiredAccessData,
} as const;
}
@ -222,21 +226,20 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
const user = sessionData?.user;
if (!data.isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={''} emailHasAccount={true} />;
return (
<DocumentSigningAuthPageView
email={data.recipientEmail}
emailHasAccount={!!data.recipientHasAccount}
/>
);
}
const { envelope, recipient } = data.envelopeForSigning;
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
});
const isEmailForced = derivedRecipientAccessAuth.includes(DocumentAccessAuth.ACCOUNT);
return (
<EnvelopeSigningProvider
envelopeData={data.envelopeForSigning}
email={isEmailForced ? user?.email || '' : ''} // Doing this allows us to let users change the email if they want to for non-auth templates.
email={''} // Doing this allows us to let users change the email if they want to.
fullName={user?.name}
signature={user?.signature}
>

13
package-lock.json generated
View File

@ -19,6 +19,7 @@
"inngest-cli": "^0.29.1",
"luxon": "^3.5.0",
"mupdf": "^1.0.0",
"pdf2json": "^4.0.0",
"react": "^18",
"typescript": "5.6.2",
"zod": "3.24.1"
@ -27198,6 +27199,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/pdf2json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pdf2json/-/pdf2json-4.0.0.tgz",
"integrity": "sha512-WkezNsLK8sGpuFC7+PPP0DsXROwdoOxmXPBTtUWWkCwCi/Vi97MRC52Ly6FWIJjOKIywpm/L2oaUgSrmtU+7ZQ==",
"license": "Apache-2.0",
"bin": {
"pdf2json": "bin/pdf2json.js"
},
"engines": {
"node": ">=20.18.0"
}
},
"node_modules/pdfjs-dist": {
"version": "3.11.174",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",

View File

@ -74,6 +74,7 @@
"inngest-cli": "^0.29.1",
"luxon": "^3.5.0",
"mupdf": "^1.0.0",
"pdf2json": "^4.0.0",
"react": "^18",
"typescript": "5.6.2",
"zod": "3.24.1"

View File

@ -1,12 +1,9 @@
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
@ -124,7 +121,7 @@ test('[DIRECT_TEMPLATES]: delete direct template link', async ({ page }) => {
await expect(page.getByText('404 not found')).toBeVisible();
});
test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page }) => {
test('[DIRECT_TEMPLATES]: direct template link auth access', async ({ page }) => {
const { user, team } = await seedUser();
const directTemplateWithAuth = await seedDirectTemplate({
@ -156,53 +153,6 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByLabel('Email')).toBeDisabled();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});
test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page }) => {
const { user, team } = await seedUser();
const directTemplateWithAuth = await seedDirectTemplate({
title: 'Personal direct template link',
userId: user.id,
teamId: team.id,
internalVersion: 2,
createTemplateOptions: {
authOptions: createDocumentAuthOptions({
globalAccessAuth: ['ACCOUNT'],
globalActionAuth: [],
}),
},
});
const directTemplatePath = formatDirectTemplatePath(
directTemplateWithAuth.directLink?.token || '',
);
await page.goto(directTemplatePath);
await expect(page.getByText('Authentication required')).toBeVisible();
await apiSignin({
page,
email: user.email,
});
await page.goto(directTemplatePath);
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByLabel('Your Email')).not.toBeVisible();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});
test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ page }) => {
@ -225,9 +175,6 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
@ -236,173 +183,3 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
// Add a longer waiting period to ensure document status is updated
await page.waitForTimeout(3000);
});
test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with next signer dictation', async ({
page,
}) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
});
// Should be visible to team members.
const template = await seedDirectTemplate({
title: 'Team direct template link 1',
userId: owner.id,
teamId: team.id,
});
await prisma.documentMeta.update({
where: {
id: template.documentMetaId,
},
data: {
allowDictateNextSigner: true,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
email: originalSecondSignerEmail,
name: originalName,
token: Math.random().toString().slice(2, 7),
role: RecipientRole.SIGNER,
},
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await page.waitForTimeout(100);
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByText('Next Recipient Name')).toBeVisible();
const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
expect(nextRecipientNameInputValue).toBe(originalName);
const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);
const newName = 'Hello';
const newSecondSignerEmail = seedTestEmail();
await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
await page.getByLabel('Next Recipient Name').fill(newName);
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
const createdEnvelopeRecipients = await prisma.recipient.findMany({
where: {
envelope: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
},
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
});
test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with next signer dictation', async ({
page,
}) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
});
// Should be visible to team members.
const template = await seedDirectTemplate({
title: 'Team direct template link 1',
userId: owner.id,
teamId: team.id,
internalVersion: 2,
});
await prisma.documentMeta.update({
where: {
id: template.documentMetaId,
},
data: {
allowDictateNextSigner: true,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
email: originalSecondSignerEmail,
name: originalName,
token: Math.random().toString().slice(2, 7),
role: RecipientRole.SIGNER,
},
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'Team direct template link 1' })).toBeVisible();
await page.waitForTimeout(100);
await page.getByRole('button', { name: 'Complete' }).click();
const currentName = 'John Doe';
const currentEmail = seedTestEmail();
await page.getByPlaceholder('Enter Your Name').fill(currentName);
await page.getByPlaceholder('Enter Your Email').fill(currentEmail);
await expect(page.getByText('Next Recipient Name')).toBeVisible();
const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
expect(nextRecipientNameInputValue).toBe(originalName);
const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);
const newName = 'Hello';
const newSecondSignerEmail = seedTestEmail();
await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
await page.getByLabel('Next Recipient Name').fill(newName);
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
const createdEnvelopeRecipients = await prisma.recipient.findMany({
where: {
envelope: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
},
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
});

View File

@ -97,9 +97,7 @@ export const completeDocumentWithToken = async ({
}
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const isRecipientsTurn = await getIsRecipientsTurnToSign({
token: recipient.token,
});
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
if (!isRecipientsTurn) {
throw new Error(
@ -153,18 +151,6 @@ export const completeDocumentWithToken = async ({
}),
});
const envelopeForFailure = await prisma.envelope.findUniqueOrThrow({
where: { id: envelope.id },
include: { documentMeta: true, recipients: true },
});
await triggerWebhook({
event: WebhookTriggerEvents.RECIPIENT_AUTHENTICATION_FAILED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeForFailure)),
userId: envelope.userId,
teamId: envelope.teamId,
});
throw new AppError(AppErrorCode.TWO_FACTOR_AUTH_FAILED, {
message: 'Invalid 2FA authentication',
});
@ -219,18 +205,6 @@ export const completeDocumentWithToken = async ({
});
});
const envelopeWithRelations = await prisma.envelope.findUniqueOrThrow({
where: { id: envelope.id },
include: { documentMeta: true, recipients: true },
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations)),
userId: envelope.userId,
teamId: envelope.teamId,
});
await jobs.triggerJob({
name: 'send.recipient.signed.email',
payload: {

View File

@ -7,7 +7,6 @@ import {
OrganisationType,
RecipientRole,
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
@ -25,16 +24,11 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { isDocumentCompleted } from '../../utils/document';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { getEmailContext } from '../email/get-email-context';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type ResendDocumentOptions = {
id: EnvelopeIdOptions;
@ -236,11 +230,4 @@ export const resendDocument = async ({
);
}),
);
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_REMINDER_SENT,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
userId: envelope.userId,
teamId: envelope.teamId,
});
};

View File

@ -1,4 +1,5 @@
import { EnvelopeType, ReadStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client';
import { WebhookTriggerEvents } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
@ -65,13 +66,6 @@ export const viewedDocument = async ({
}),
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_VIEWED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
userId: envelope.userId,
teamId: envelope.teamId,
});
// Early return if already opened.
if (recipient.readStatus === ReadStatus.OPENED) {
return;

View File

@ -386,13 +386,6 @@ export const createEnvelope = async ({
userId,
teamId,
});
} else if (type === EnvelopeType.TEMPLATE) {
await triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_CREATED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
userId,
teamId,
});
}
return createdEnvelope;

View File

@ -1,11 +1,10 @@
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import type { TDocumentAuthMethods } from '../../types/document-auth';
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
import { getTeamSettings } from '../team/get-team-settings';
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
@ -99,28 +98,14 @@ export const getEnvelopeForDirectTemplateSigning = async ({
});
}
// Currently not using this since for direct templates "User" access means they just need to be
// logged in.
// const documentAccessValid = await isRecipientAuthorized({
// type: 'ACCESS',
// documentAuthOptions: envelope.authOptions,
// recipient,
// userId,
// authOptions: accessAuth,
// });
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
const documentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
documentAuthOptions: envelope.authOptions,
recipient,
userId,
authOptions: accessAuth,
});
// Ensure typesafety when we add more options.
const documentAccessValid = derivedRecipientAccessAuth.every((auth) =>
match(auth)
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(userId))
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true)
.exhaustive(),
);
if (!documentAccessValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid access values',

View File

@ -54,3 +54,54 @@ export const getEnvelopeRequiredAccessData = async ({ token }: { token: string }
recipientHasAccount: Boolean(recipientUserAccount),
} as const;
};
export const getEnvelopeDirectTemplateRequiredAccessData = async ({ token }: { token: string }) => {
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.TEMPLATE,
directLink: {
enabled: true,
token,
},
status: DocumentStatus.DRAFT,
},
include: {
recipients: {
where: {
token,
},
},
directLink: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const recipient = envelope.recipients.find(
(r) => r.id === envelope.directLink?.directTemplateRecipientId,
);
if (!recipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
const recipientUserAccount = await prisma.user.findFirst({
where: {
email: recipient.email.toLowerCase(),
},
select: {
id: true,
},
});
return {
recipientEmail: recipient.email,
recipientHasAccount: Boolean(recipientUserAccount),
} as const;
};

View File

@ -1,5 +1,6 @@
import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client';
import { DocumentStatus, EnvelopeType, FolderType, WebhookTriggerEvents } from '@prisma/client';
import { EnvelopeType, FolderType } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { isDeepEqual } from 'remeda';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@ -11,14 +12,9 @@ import { prisma } from '@documenso/prisma';
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getEnvelopeWhereInput } from './get-envelope-by-id';
export type UpdateEnvelopeOptions = {
@ -343,22 +339,6 @@ export const updateEnvelope = async ({
});
}
if (envelope.type === EnvelopeType.TEMPLATE) {
const envelopeWithRelations = await tx.envelope.findUniqueOrThrow({
where: { id: updatedEnvelope.id },
include: { documentMeta: true, recipients: true },
});
void triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_UPDATED,
data: ZWebhookDocumentSchema.parse(
mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations),
),
userId,
teamId,
});
}
return updatedEnvelope;
});
};

View File

@ -0,0 +1,482 @@
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
import type { Recipient } from '@prisma/client';
import { EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
import PDFParser from 'pdf2json';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields';
import { createDocumentRecipients } from '@documenso/lib/server-only/recipient/create-document-recipients';
import { createTemplateRecipients } from '@documenso/lib/server-only/recipient/create-template-recipients';
import { type TFieldAndMeta, ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import { getPageSize } from './get-page-size';
type TextPosition = {
text: string;
x: number;
y: number;
w: number;
};
type CharIndexMapping = {
textPosIndex: number;
};
type PlaceholderInfo = {
placeholder: string;
recipient: string;
fieldAndMeta: TFieldAndMeta;
page: number;
x: number;
y: number;
width: number;
height: number;
pageWidth: number;
pageHeight: number;
};
type FieldToCreate = TFieldAndMeta & {
recipientId: number;
pageNumber: number;
pageX: number;
pageY: number;
width: number;
height: number;
};
/*
Questions for later:
- Does it handle multi-page PDFs? ✅ YES! ✅
- Does it handle multiple recipients on the same page? ✅ YES! ✅
- Does it handle multiple recipients on multiple pages? ✅ YES! ✅
- What happens with incorrect placeholders? E.g. those containing non-accepted properties.
- The placeholder data is dynamic. How to handle this parsing? Perhaps we need to do it similar to the fieldMeta parsing.
- Need to handle envelopes with multiple items.
*/
/*
Parse field type string to FieldType enum.
Normalizes the input (uppercase, trim) and validates it's a valid field type.
This ensures we handle case variations and whitespace, and provides clear error messages.
*/
const parseFieldType = (fieldTypeString: string): FieldType => {
const normalizedType = fieldTypeString.toUpperCase().trim();
return match(normalizedType)
.with('SIGNATURE', () => FieldType.SIGNATURE)
.with('FREE_SIGNATURE', () => FieldType.FREE_SIGNATURE)
.with('INITIALS', () => FieldType.INITIALS)
.with('NAME', () => FieldType.NAME)
.with('EMAIL', () => FieldType.EMAIL)
.with('DATE', () => FieldType.DATE)
.with('TEXT', () => FieldType.TEXT)
.with('NUMBER', () => FieldType.NUMBER)
.with('RADIO', () => FieldType.RADIO)
.with('CHECKBOX', () => FieldType.CHECKBOX)
.with('DROPDOWN', () => FieldType.DROPDOWN)
.otherwise(() => {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field type: ${fieldTypeString}`,
});
});
};
/*
Transform raw field metadata from placeholder format to schema format.
Users should provide properly capitalized property names (e.g., readOnly, fontSize, textAlign).
Converts string values to proper types (booleans, numbers).
*/
const parseFieldMeta = (
rawFieldMeta: Record<string, string>,
fieldType: FieldType,
): Record<string, unknown> | undefined => {
if (fieldType === FieldType.SIGNATURE || fieldType === FieldType.FREE_SIGNATURE) {
return;
}
if (Object.keys(rawFieldMeta).length === 0) {
return;
}
const fieldTypeString = String(fieldType).toLowerCase();
const parsedFieldMeta: Record<string, boolean | number | string> = {
type: fieldTypeString,
};
/*
rawFieldMeta is an object with string keys and string values.
It contains string values because the PDF parser returns the values as strings.
E.g. { required: 'true', fontSize: '12', maxValue: '100', minValue: '0', characterLimit: '100' }
*/
const rawFieldMetaEntries = Object.entries(rawFieldMeta);
for (const entry of rawFieldMetaEntries) {
const [key, value] = entry;
if (key === 'readOnly' || key === 'required') {
parsedFieldMeta[key] = value === 'true';
} else if (
key === 'fontSize' ||
key === 'maxValue' ||
key === 'minValue' ||
key === 'characterLimit'
) {
const numValue = Number(value);
if (!Number.isNaN(numValue)) {
parsedFieldMeta[key] = numValue;
}
} else {
parsedFieldMeta[key] = value;
}
}
return parsedFieldMeta;
};
export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<PlaceholderInfo[]> => {
return new Promise((resolve, reject) => {
const parser = new PDFParser(null, true);
parser.on('pdfParser_dataError', (errData) => {
reject(errData);
});
parser.on('pdfParser_dataReady', (pdfData) => {
const placeholders: PlaceholderInfo[] = [];
pdfData.Pages.forEach((page, pageIndex) => {
/*
pdf2json returns the PDF page content as an array of characters.
We need to concatenate the characters to get the full text.
We also need to get the position of the text so we can place the placeholders in the correct position.
Page dimensions from PDF2JSON are in "page units" (relative coordinates)
*/
const pageWidth = page.Width;
const pageHeight = page.Height;
let pageText = '';
const textPositions: TextPosition[] = [];
const charIndexToTextPos: CharIndexMapping[] = [];
page.Texts.forEach((text) => {
/*
R is an array that contains objects with each character.
The decodedText contains only the character, without any other information.
textPositions stores each character and its position on the page.
*/
const decodedText = text.R.map((run) => decodeURIComponent(run.T)).join('');
for (let i = 0; i < decodedText.length; i++) {
charIndexToTextPos.push({
textPosIndex: textPositions.length,
});
}
pageText += decodedText;
textPositions.push({
text: decodedText,
x: text.x,
y: text.y,
w: text.w || 0,
});
});
const placeholderMatches = pageText.matchAll(/{{([^}]+)}}/g);
for (const match of placeholderMatches) {
const placeholder = match[0];
const placeholderData = match[1].split(',').map((part) => part.trim());
const [fieldTypeString, recipient, ...fieldMetaData] = placeholderData;
const rawFieldMeta = Object.fromEntries(fieldMetaData.map((meta) => meta.split('=')));
const fieldType = parseFieldType(fieldTypeString);
const parsedFieldMeta = parseFieldMeta(rawFieldMeta, fieldType);
const fieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({
type: fieldType,
fieldMeta: parsedFieldMeta,
});
/*
Find the position of where the placeholder starts in the text
Then find the position of where the placeholder ends in the text by adding the length of the placeholder to the index of the placeholder.
*/
const matchIndex = match.index;
const placeholderLength = placeholder.length;
const placeholderEndIndex = matchIndex + placeholderLength;
const startCharInfo = charIndexToTextPos[matchIndex];
const endCharInfo = charIndexToTextPos[placeholderEndIndex - 1];
if (!startCharInfo || !endCharInfo) {
console.error('Could not find text position for placeholder', placeholder);
return;
}
const startTextPos = textPositions[startCharInfo.textPosIndex];
const endTextPos = textPositions[endCharInfo.textPosIndex];
/*
PDF2JSON coordinates - these are in "page units" (relative coordinates)
Calculate width as the distance from start to end, plus a portion of the last character's width
Use 10% of the last character width to avoid extending too far beyond the placeholder
*/
const x = startTextPos.x;
const y = startTextPos.y;
const width = endTextPos.x + endTextPos.w * 0.1 - startTextPos.x;
placeholders.push({
placeholder,
recipient,
fieldAndMeta,
page: pageIndex + 1,
x,
y,
width,
height: 1,
pageWidth,
pageHeight,
});
}
});
resolve(placeholders);
});
parser.parseBuffer(pdf);
});
};
export const replacePlaceholdersInPDF = async (pdf: Buffer): Promise<Buffer> => {
const placeholders = await extractPlaceholdersFromPDF(pdf);
const pdfDoc = await PDFDocument.load(new Uint8Array(pdf));
const pages = pdfDoc.getPages();
for (const placeholder of placeholders) {
const pageIndex = placeholder.page - 1;
const page = pages[pageIndex];
const { width: pdfLibPageWidth, height: pdfLibPageHeight } = getPageSize(page);
/*
Convert PDF2JSON coordinates to pdf-lib coordinates:
PDF2JSON uses relative "page units":
- x, y, width, height are in page units
- Page dimensions (Width, Height) are also in page units
pdf-lib uses absolute points (1 point = 1/72 inch):
- Need to convert from page units to points
- Y-axis in pdf-lib is bottom-up (origin at bottom-left)
- Y-axis in PDF2JSON is top-down (origin at top-left)
Conversion formulas:
- x_points = (x / pageWidth) * pdfLibPageWidth
- y_points = pdfLibPageHeight - ((y / pageHeight) * pdfLibPageHeight)
- width_points = (width / pageWidth) * pdfLibPageWidth
- height_points = (height / pageHeight) * pdfLibPageHeight
*/
const xPoints = (placeholder.x / placeholder.pageWidth) * pdfLibPageWidth;
const yPoints = pdfLibPageHeight - (placeholder.y / placeholder.pageHeight) * pdfLibPageHeight;
const widthPoints = (placeholder.width / placeholder.pageWidth) * pdfLibPageWidth;
const heightPoints = (placeholder.height / placeholder.pageHeight) * pdfLibPageHeight;
page.drawRectangle({
x: xPoints,
y: yPoints - heightPoints, // Adjust for height since y is at baseline
width: widthPoints,
height: heightPoints,
color: rgb(1, 1, 1),
borderColor: rgb(1, 1, 1),
borderWidth: 2,
});
}
const modifiedPdfBytes = await pdfDoc.save();
return Buffer.from(modifiedPdfBytes);
};
const extractRecipientPlaceholder = (
placeholder: string,
): { email: string; recipientIndex: number } => {
const indexMatch = placeholder.match(/^r(\d+)$/i);
if (!indexMatch) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid recipient placeholder format: ${placeholder}. Expected format: r1, r2, r3, etc.`,
});
}
return {
email: `recipient.${indexMatch[1]}@documenso.com`,
recipientIndex: Number(indexMatch[1]),
};
};
export const insertFieldsFromPlaceholdersInPDF = async (
pdf: Buffer,
userId: number,
teamId: number,
envelopeId: EnvelopeIdOptions,
requestMetadata: ApiRequestMetadata,
): Promise<Buffer> => {
const placeholders = await extractPlaceholdersFromPDF(pdf);
if (placeholders.length === 0) {
return pdf;
}
/*
A structure that maps the recipient email to the recipient index.
Example: 'recipient.1@documenso.com' => 1
*/
const recipientEmailToIndex = new Map<string, number>();
for (const placeholder of placeholders) {
const { email, recipientIndex } = extractRecipientPlaceholder(placeholder.recipient);
recipientEmailToIndex.set(email, recipientIndex);
}
/*
Create a list of recipients to create.
Example: [{ email: 'recipient.1@documenso.com', name: 'Recipient 1', role: 'SIGNER', signingOrder: 1 }]
*/
const recipientsToCreate = Array.from(
recipientEmailToIndex.entries(),
([email, recipientIndex]) => {
const placeholderInfo = generateRecipientPlaceholder(recipientIndex);
return {
email,
name: placeholderInfo.name,
role: RecipientRole.SIGNER,
signingOrder: recipientIndex,
};
},
);
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: envelopeId,
userId,
teamId,
type: null,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
select: {
id: true,
type: true,
secondaryId: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
let createdRecipients: Pick<Recipient, 'id' | 'email'>[];
if (envelope.type === EnvelopeType.DOCUMENT) {
const { recipients } = await createDocumentRecipients({
userId,
teamId,
id: envelopeId,
recipients: recipientsToCreate,
requestMetadata,
});
createdRecipients = recipients;
} else if (envelope.type === EnvelopeType.TEMPLATE) {
const templateId =
envelopeId.type === 'templateId'
? envelopeId.id
: mapSecondaryIdToTemplateId(envelope.secondaryId);
const { recipients } = await createTemplateRecipients({
userId,
teamId,
templateId,
recipients: recipientsToCreate,
});
createdRecipients = recipients;
} else {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid envelope type: ${envelope.type}`,
});
}
const fieldsToCreate: FieldToCreate[] = [];
for (const placeholder of placeholders) {
/*
Convert PDF2JSON coordinates to percentage-based coordinates (0-100)
The UI expects positionX and positionY as percentages, not absolute points
PDF2JSON uses relative coordinates: x/pageWidth and y/pageHeight give us the percentage
*/
const xPercent = (placeholder.x / placeholder.pageWidth) * 100;
const yPercent = (placeholder.y / placeholder.pageHeight) * 100;
const widthPercent = (placeholder.width / placeholder.pageWidth) * 100;
const heightPercent = (placeholder.height / placeholder.pageHeight) * 100;
const { email } = extractRecipientPlaceholder(placeholder.recipient);
const normalizedEmail = email.toLowerCase();
const recipient = createdRecipients.find((r) => r.email.toLowerCase() === normalizedEmail);
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Could not find recipient ID for placeholder: ${placeholder.placeholder}`,
});
}
const recipientId = recipient.id;
// Default height percentage if too small (use 2% as a reasonable default)
const finalHeightPercent = heightPercent > 0.01 ? heightPercent : 2;
fieldsToCreate.push({
...placeholder.fieldAndMeta,
recipientId,
pageNumber: placeholder.page,
pageX: xPercent,
pageY: yPercent,
width: widthPercent,
height: finalHeightPercent,
});
}
await createEnvelopeFields({
userId,
teamId,
id: envelopeId,
fields: fieldsToCreate,
requestMetadata,
});
return pdf;
};

View File

@ -1,5 +1,6 @@
import { PDFDocument } from '@cantoo/pdf-lib';
import { replacePlaceholdersInPDF } from './auto-place-fields';
import { flattenAnnotations } from './flatten-annotations';
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
@ -13,6 +14,7 @@ export const normalizePdf = async (pdf: Buffer) => {
removeOptionalContentGroups(pdfDoc);
await flattenForm(pdfDoc);
flattenAnnotations(pdfDoc);
const pdfWithoutPlaceholders = await replacePlaceholdersInPDF(pdf);
return Buffer.from(await pdfDoc.save());
return pdfWithoutPlaceholders;
};

View File

@ -3,7 +3,6 @@ import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import type { Field, Signature } from '@prisma/client';
import {
DocumentSigningOrder,
DocumentSource,
DocumentStatus,
EnvelopeType,
@ -27,7 +26,7 @@ import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/f
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import { ZFieldMetaSchema } from '../../types/field-meta';
@ -69,10 +68,6 @@ export type CreateDocumentFromDirectTemplateOptions = {
name?: string;
email: string;
};
nextSigner?: {
email: string;
name: string;
};
};
type CreatedDirectRecipientField = {
@ -97,7 +92,6 @@ export const createDocumentFromDirectTemplate = async ({
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
nextSigner,
requestMetadata,
user,
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
@ -134,17 +128,6 @@ export const createDocumentFromDirectTemplate = async ({
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
}
if (
nextSigner &&
(!directTemplateEnvelope.documentMeta?.allowDictateNextSigner ||
directTemplateEnvelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
});
}
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
directTemplateEnvelope.secondaryId,
);
@ -647,77 +630,6 @@ export const createDocumentFromDirectTemplate = async ({
}),
];
if (nextSigner) {
const pendingRecipients = await tx.recipient.findMany({
select: {
id: true,
signingOrder: true,
name: true,
email: true,
role: true,
},
where: {
envelopeId: createdEnvelope.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
role: {
not: RecipientRole.CC,
},
},
// Composite sort so our next recipient is always the one with the lowest signing order or id
// if there is a tie.
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
});
const nextRecipient = pendingRecipients[0];
if (nextRecipient) {
auditLogsToCreate.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
envelopeId: createdEnvelope.id,
user: {
name: user?.name || directRecipientName || '',
email: user?.email || directRecipientEmail,
},
metadata: requestMetadata,
data: {
recipientEmail: nextRecipient.email,
recipientName: nextRecipient.name,
recipientId: nextRecipient.id,
recipientRole: nextRecipient.role,
changes: [
{
type: RECIPIENT_DIFF_TYPE.NAME,
from: nextRecipient.name,
to: nextSigner.name,
},
{
type: RECIPIENT_DIFF_TYPE.EMAIL,
from: nextRecipient.email,
to: nextSigner.email,
},
],
},
}),
);
await tx.recipient.update({
where: { id: nextRecipient.id },
data: {
sendStatus: SendStatus.SENT,
...(nextSigner && documentMeta?.allowDictateNextSigner
? {
name: nextSigner.name,
email: nextSigner.email,
}
: {}),
},
});
}
}
await tx.documentAuditLog.createMany({
data: auditLogsToCreate,
});

View File

@ -725,13 +725,6 @@ export const createDocumentFromTemplate = async ({
teamId,
});
await triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_USED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
userId,
teamId,
});
return envelope;
});
};

View File

@ -1,14 +1,9 @@
import { EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { type EnvelopeIdOptions } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type DeleteTemplateOptions = {
id: EnvelopeIdOptions;
@ -24,18 +19,6 @@ export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptio
teamId,
});
const templateToDelete = await prisma.envelope.findUniqueOrThrow({
where: envelopeWhereInput,
include: { documentMeta: true, recipients: true },
});
await triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_DELETED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(templateToDelete)),
userId,
teamId,
});
return await prisma.envelope.delete({
where: envelopeWhereInput,
});

View File

@ -480,198 +480,5 @@ export const generateSampleWebhookPayload = (
};
}
if (event === WebhookTriggerEvents.DOCUMENT_VIEWED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_DOWNLOADED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.COMPLETED,
completedAt: now,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_REMINDER_SENT) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
Recipient: [
{
...basePayload.recipients[0],
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.RECIPIENT_AUTHENTICATION_FAILED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_CREATED) {
return {
event,
payload: {
...basePayload,
title: 'My Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_UPDATED) {
return {
event,
payload: {
...basePayload,
title: 'My Updated Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_DELETED) {
return {
event,
payload: {
...basePayload,
title: 'Deleted Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_USED) {
return {
event,
payload: {
...basePayload,
title: 'Document from Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
throw new Error(`Unsupported event type: ${event}`);
};

View File

@ -1,17 +0,0 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_VIEWED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_RECIPIENT_COMPLETED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_DOWNLOADED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_REMINDER_SENT';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_CREATED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_UPDATED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_DELETED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_USED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'RECIPIENT_AUTHENTICATION_FAILED';

View File

@ -172,15 +172,6 @@ enum WebhookTriggerEvents {
DOCUMENT_COMPLETED
DOCUMENT_REJECTED
DOCUMENT_CANCELLED
DOCUMENT_VIEWED
DOCUMENT_RECIPIENT_COMPLETED
DOCUMENT_DOWNLOADED
DOCUMENT_REMINDER_SENT
TEMPLATE_CREATED
TEMPLATE_UPDATED
TEMPLATE_DELETED
TEMPLATE_USED
RECIPIENT_AUTHENTICATION_FAILED
}
model Webhook {

View File

@ -28,7 +28,6 @@ type SeedTemplateOptions = {
title?: string;
userId: number;
teamId: number;
internalVersion?: 1 | 2;
createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>;
};
@ -168,7 +167,7 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
data: {
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: options.internalVersion ?? 1,
internalVersion: 1,
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: {
@ -185,7 +184,6 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
teamId,
recipients: {
create: {
signingOrder: 1,
email: DIRECT_TEMPLATE_RECIPIENT_EMAIL,
name: DIRECT_TEMPLATE_RECIPIENT_NAME,
token: Math.random().toString().slice(2, 7),

View File

@ -1,13 +1,8 @@
import type { DocumentData } from '@prisma/client';
import { DocumentDataType, EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { triggerWebhook } from '@documenso/lib/server-only/webhooks/trigger/trigger-webhook';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '@documenso/lib/types/webhook-payload';
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
@ -81,13 +76,6 @@ export const downloadDocumentRoute = authenticatedProcedure
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
void triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_DOWNLOADED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
userId: envelope.userId,
teamId: envelope.teamId,
});
return {
downloadUrl: url,
filename,

View File

@ -519,7 +519,6 @@ export const templateRouter = router({
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
nextSigner,
} = input;
ctx.logger.info({
@ -542,7 +541,6 @@ export const templateRouter = router({
email: ctx.user.email,
}
: undefined,
nextSigner,
requestMetadata: ctx.metadata,
});
}),

View File

@ -90,12 +90,6 @@ export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
directTemplateExternalId: z.string().optional(),
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
templateUpdatedAt: z.date(),
nextSigner: z
.object({
email: z.string().email().max(254),
name: z.string().min(1).max(255),
})
.optional(),
});
export const ZCreateDocumentFromTemplateRequestSchema = z.object({