feat: add direct templates links (#1165)

## Description

Direct templates links is a feature that provides template owners the
ability to allow users to create documents based of their templates.

## General outline

This works by allowing the template owner to configure a "direct
recipient" in the template.

When a user opens the direct link to the template, it will create a flow
where they sign the fields configured by the template owner for the
direct recipient. After these fields are signed the following will
occur:

- A document will be created where the owner is the template owner
- The direct recipient fields will be signed
- The document will be sent to any other recipients configured in the
template
- If there are none the document will be immediately completed

## Notes

There's a custom prisma migration to migrate all documents to have
'DOCUMENT' as the source, then sets the column to required.

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
This commit is contained in:
David Nguyen
2024-06-02 15:49:09 +10:00
committed by GitHub
parent c346a3fd6a
commit d11a68fc4c
71 changed files with 3636 additions and 283 deletions

View File

@@ -5,7 +5,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { DocumentSource, WebhookTriggerEvents } from '@documenso/prisma/client';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -54,6 +54,7 @@ export const createDocument = async ({
userId,
teamId,
formValues,
source: DocumentSource.DOCUMENT,
},
});
@@ -65,6 +66,9 @@ export const createDocument = async ({
requestMetadata,
data: {
title,
source: {
type: DocumentSource.DOCUMENT,
},
},
}),
});

View File

@@ -1,5 +1,5 @@
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { DocumentSource, type Prisma } from '@documenso/prisma/client';
import { getDocumentWhereInput } from './get-document-by-id';
@@ -64,6 +64,7 @@ export const duplicateDocumentById = async ({
...document.documentMeta,
},
},
source: DocumentSource.DOCUMENT,
},
};

View File

@@ -99,7 +99,7 @@ export const getDocumentAndSenderByToken = async ({
if (requireAccessAuth) {
documentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
document: result,
documentAuthOptions: result.authOptions,
recipient,
userId,
authOptions: accessAuth,
@@ -159,7 +159,7 @@ export const getDocumentAndRecipientByToken = async ({
if (requireAccessAuth) {
documentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
document: result,
documentAuthOptions: result.authOptions,
recipient,
userId,
authOptions: accessAuth,

View File

@@ -14,8 +14,8 @@ import { extractDocumentAuthMethods } from '../../utils/document-auth';
type IsRecipientAuthorizedOptions = {
type: 'ACCESS' | 'ACTION';
document: Document;
recipient: Recipient;
documentAuthOptions: Document['authOptions'];
recipient: Pick<Recipient, 'authOptions' | 'email'>;
/**
* The ID of the user who initiated the request.
@@ -50,13 +50,13 @@ const getUserByEmail = async (email: string) => {
*/
export const isRecipientAuthorized = async ({
type,
document,
documentAuthOptions,
recipient,
userId,
authOptions,
}: IsRecipientAuthorizedOptions): Promise<boolean> => {
const { derivedRecipientAccessAuth, derivedRecipientActionAuth } = extractDocumentAuthMethods({
documentAuth: document.authOptions,
documentAuth: documentAuthOptions,
recipientAuth: recipient.authOptions,
});

View File

@@ -12,7 +12,13 @@ import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
import {
DocumentSource,
DocumentStatus,
RecipientRole,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
@@ -92,6 +98,8 @@ export const sendDocument = async ({
const { documentData } = document;
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
if (!documentData.data) {
throw new Error('Document data not found');
}
@@ -133,10 +141,21 @@ export const sendDocument = async ({
const { email, name } = recipient;
const selfSigner = email === user.email;
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
const recipientActionVerb = actionVerb.toLowerCase();
const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[
recipient.role
].actionVerb.toLowerCase()} it.`;
let emailMessage = customEmail?.message || '';
let emailSubject = `Please ${recipientActionVerb} this document`;
if (selfSigner) {
emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
emailSubject = `Please ${recipientActionVerb} your document`;
}
if (isDirectTemplate) {
emailMessage = `A document was created by your direct template that requires you to ${recipientActionVerb} it.`;
emailSubject = `Please ${recipientActionVerb} this document created by your direct template`;
}
const customEmailTemplate = {
'signer.name': name,
@@ -153,22 +172,11 @@ export const sendDocument = async ({
inviterEmail: user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(
selfSigner && !customEmail?.message
? selfSignerCustomEmail
: customEmail?.message || '',
customEmailTemplate,
),
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
selfSigner,
});
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
const emailSubject = selfSigner
? `Please ${actionVerb.toLowerCase()} your document`
: `Please ${actionVerb.toLowerCase()} this document`;
await prisma.$transaction(
async (tx) => {
await mailer.sendMail({
@@ -220,7 +228,8 @@ export const sendDocument = async ({
}
const allRecipientsHaveNoActionToTake = document.Recipient.every(
(recipient) => recipient.role === RecipientRole.CC,
(recipient) =>
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
);
if (allRecipientsHaveNoActionToTake) {

View File

@@ -0,0 +1,52 @@
import type { Document, Field, Recipient } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TRecipientActionAuth } from '../../types/document-auth';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { isRecipientAuthorized } from './is-recipient-authorized';
export type ValidateFieldAuthOptions = {
documentAuthOptions: Document['authOptions'];
recipient: Pick<Recipient, 'authOptions' | 'email'>;
field: Field;
userId?: number;
authOptions?: TRecipientActionAuth;
};
/**
* Throws an error if the reauth for a field is invalid.
*
* Returns the derived recipient action authentication if valid.
*/
export const validateFieldAuth = async ({
documentAuthOptions,
recipient,
field,
userId,
authOptions,
}: ValidateFieldAuthOptions) => {
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
documentAuth: documentAuthOptions,
recipientAuth: recipient.authOptions,
});
// Override all non-signature fields to not require any auth.
if (field.type !== FieldType.SIGNATURE) {
return null;
}
const isValid = await isRecipientAuthorized({
type: 'ACTION',
documentAuthOptions,
recipient,
userId,
authOptions,
});
if (!isValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
}
return derivedRecipientActionAuth;
};