mirror of
https://github.com/documenso/documenso.git
synced 2026-07-07 11:35:01 +10:00
d5ce222482
Adds Cloud Signature Consortium (CSC) integration for AES/QES signing against a configured TSP. v1 ships as instance-wide configuration via environment variables, with per-envelope signature level selection, license gating, and an OAuth-driven signing flow (capture + FIFO signers, SAD session, blocking/in-progress recipient pages). Includes signature level compatibility checks (role, signing order, dictate next signer), envelope mutability assertions, Prisma migration for signature level and CSC tables, and docs for the new signing certificate options.
815 lines
28 KiB
TypeScript
815 lines
28 KiB
TypeScript
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
|
import { prisma } from '@documenso/prisma';
|
|
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
|
|
import type { Field, Signature } from '@prisma/client';
|
|
import {
|
|
DocumentSigningOrder,
|
|
DocumentSource,
|
|
DocumentStatus,
|
|
EnvelopeType,
|
|
FieldType,
|
|
Prisma,
|
|
RecipientRole,
|
|
SendStatus,
|
|
SigningStatus,
|
|
WebhookTriggerEvents,
|
|
} from '@prisma/client';
|
|
import { DateTime } from 'luxon';
|
|
import { match } from 'ts-pattern';
|
|
import { z } from 'zod';
|
|
|
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
import { jobs } from '../../jobs/client';
|
|
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
|
|
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
|
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
|
import { ZFieldMetaSchema } from '../../types/field-meta';
|
|
import { ZSignatureLevelSchema } from '../../types/signature-level';
|
|
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
|
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
|
import { isRequiredField } from '../../utils/advanced-fields-helpers';
|
|
import { extractDerivedDocumentMeta } from '../../utils/document';
|
|
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|
import {
|
|
createDocumentAuthOptions,
|
|
createRecipientAuthOptions,
|
|
extractDocumentAuthMethods,
|
|
} from '../../utils/document-auth';
|
|
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
|
import { sendDocument } from '../document/send-document';
|
|
import { validateFieldAuth } from '../document/validate-field-auth';
|
|
import { incrementDocumentId } from '../envelope/increment-id';
|
|
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
|
|
import { resolveSignatureLevel } from '../signature-level/resolve-signature-level';
|
|
import { getTeamSettings } from '../team/get-team-settings';
|
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|
|
|
export type CreateDocumentFromDirectTemplateOptions = {
|
|
directRecipientName?: string;
|
|
directRecipientEmail: string;
|
|
directTemplateToken: string;
|
|
directTemplateExternalId?: string;
|
|
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
|
templateUpdatedAt: Date;
|
|
requestMetadata: ApiRequestMetadata;
|
|
user?: {
|
|
id: number;
|
|
name?: string;
|
|
email: string;
|
|
};
|
|
nextSigner?: {
|
|
email: string;
|
|
name: string;
|
|
};
|
|
};
|
|
|
|
type CreatedDirectRecipientField = {
|
|
field: Field & { signature?: Signature | null };
|
|
derivedRecipientActionAuth?: TRecipientActionAuthTypes;
|
|
};
|
|
|
|
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
|
token: z.string(),
|
|
envelopeId: z.string(),
|
|
documentId: z.number(),
|
|
recipientId: z.number(),
|
|
});
|
|
|
|
export type TCreateDocumentFromDirectTemplateResponse = z.infer<typeof ZCreateDocumentFromDirectTemplateResponseSchema>;
|
|
|
|
export const createDocumentFromDirectTemplate = async ({
|
|
directRecipientName: initialDirectRecipientName,
|
|
directRecipientEmail,
|
|
directTemplateToken,
|
|
directTemplateExternalId,
|
|
signedFieldValues,
|
|
templateUpdatedAt,
|
|
nextSigner,
|
|
requestMetadata,
|
|
user,
|
|
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
|
|
const directTemplateEnvelope = await prisma.envelope.findFirst({
|
|
where: {
|
|
directLink: {
|
|
token: directTemplateToken,
|
|
},
|
|
},
|
|
include: {
|
|
recipients: {
|
|
include: {
|
|
fields: true,
|
|
},
|
|
},
|
|
directLink: true,
|
|
envelopeItems: {
|
|
include: {
|
|
documentData: true,
|
|
},
|
|
},
|
|
documentMeta: true,
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
},
|
|
},
|
|
team: {
|
|
select: {
|
|
organisationId: true,
|
|
organisation: {
|
|
select: {
|
|
organisationClaim: {
|
|
select: {
|
|
recipientCount: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!directTemplateEnvelope?.directLink?.enabled) {
|
|
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);
|
|
|
|
if (directTemplateEnvelope.envelopeItems.length < 1) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Invalid number of envelope items',
|
|
});
|
|
}
|
|
|
|
const settings = await getTeamSettings({
|
|
userId: directTemplateEnvelope.userId,
|
|
teamId: directTemplateEnvelope.teamId,
|
|
});
|
|
|
|
const { recipients, directLink } = directTemplateEnvelope;
|
|
|
|
const directTemplateRecipient = recipients.find((recipient) => recipient.id === directLink.directTemplateRecipientId);
|
|
|
|
if (!directTemplateRecipient || directTemplateRecipient.role === RecipientRole.CC) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'Invalid or missing direct recipient',
|
|
});
|
|
}
|
|
|
|
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
|
}
|
|
|
|
const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } = extractDocumentAuthMethods({
|
|
documentAuth: directTemplateEnvelope.authOptions,
|
|
});
|
|
|
|
let directRecipientName = user?.name || initialDirectRecipientName;
|
|
|
|
// Ensure typesafety when we add more options.
|
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
|
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => false) // Not supported for direct templates
|
|
.with(undefined, () => true)
|
|
.exhaustive();
|
|
|
|
if (!isAccessAuthValid) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You must be logged in' });
|
|
}
|
|
|
|
const directTemplateRecipientAuthOptions = ZRecipientAuthOptionsSchema.parse(directTemplateRecipient.authOptions);
|
|
|
|
const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
|
|
(recipient) => recipient.id !== directTemplateRecipient.id,
|
|
);
|
|
|
|
// Carry the template's level forward, coercing if the instance mode has
|
|
// changed since the template was created. ZSignatureLevelSchema parses the
|
|
// free-form TEXT column defensively. Resolved before meta extraction so
|
|
// signingOrder picks up the TSP-appropriate default + assertion.
|
|
const signatureLevel = resolveSignatureLevel({
|
|
requested: ZSignatureLevelSchema.parse(directTemplateEnvelope.signatureLevel),
|
|
strict: false,
|
|
});
|
|
|
|
const derivedDocumentMeta = extractDerivedDocumentMeta(settings, directTemplateEnvelope.documentMeta, signatureLevel);
|
|
|
|
// The resulting document contains every non-direct template recipient plus the
|
|
// direct recipient that is signing now. A recipientCount of 0 means unlimited.
|
|
// This mirrors the check in `sendDocument`, but must be done here because this
|
|
// flow creates the document directly in PENDING and swallows `sendDocument` errors.
|
|
const maximumRecipientCount = directTemplateEnvelope.team.organisation.organisationClaim.recipientCount;
|
|
const resultingRecipientCount = nonDirectTemplateRecipients.length + 1;
|
|
|
|
if (maximumRecipientCount > 0 && resultingRecipientCount > maximumRecipientCount) {
|
|
throw new AppError('RECIPIENT_LIMIT_EXCEEDED', {
|
|
message: `You cannot send a document with more than ${maximumRecipientCount} recipients`,
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
|
// Only process fields that are either required or have been signed by the user
|
|
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
|
|
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
|
|
|
// Custom logic for V2 to include all fields, since v1 excludes read only
|
|
// and prefilled fields.
|
|
if (directTemplateEnvelope.internalVersion === 2) {
|
|
return true;
|
|
}
|
|
|
|
// Include if it's required or has a signed value
|
|
return isRequiredField(templateField) || signedFieldValue !== undefined;
|
|
});
|
|
|
|
const createDirectRecipientFieldArgs = await Promise.all(
|
|
fieldsToProcess.map(async (templateField) => {
|
|
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
|
|
|
if (isRequiredField(templateField) && !signedFieldValue) {
|
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
message: 'Invalid, missing or changed fields',
|
|
});
|
|
}
|
|
|
|
if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
|
|
directRecipientName = signedFieldValue?.value;
|
|
}
|
|
|
|
const derivedRecipientActionAuth = await validateFieldAuth({
|
|
documentAuthOptions: directTemplateEnvelope.authOptions,
|
|
recipient: {
|
|
authOptions: directTemplateRecipient.authOptions,
|
|
email: directRecipientEmail,
|
|
envelopeId: directTemplateEnvelope.id,
|
|
},
|
|
field: templateField,
|
|
userId: user?.id,
|
|
authOptions: signedFieldValue?.authOptions,
|
|
});
|
|
|
|
if (!signedFieldValue) {
|
|
return {
|
|
templateField,
|
|
customText: '',
|
|
derivedRecipientActionAuth,
|
|
signature: null,
|
|
};
|
|
}
|
|
|
|
const { value, isBase64 } = signedFieldValue;
|
|
|
|
const isSignatureField =
|
|
templateField.type === FieldType.SIGNATURE || templateField.type === FieldType.FREE_SIGNATURE;
|
|
|
|
let customText = !isSignatureField ? value : '';
|
|
|
|
const signatureImageAsBase64 = isSignatureField && isBase64 ? value : undefined;
|
|
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
|
|
|
|
if (templateField.type === FieldType.DATE) {
|
|
customText = DateTime.now().setZone(derivedDocumentMeta.timezone).toFormat(derivedDocumentMeta.dateFormat);
|
|
}
|
|
|
|
if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
|
|
throw new Error('Signature field must have a signature');
|
|
}
|
|
|
|
return {
|
|
templateField,
|
|
customText,
|
|
derivedRecipientActionAuth,
|
|
signature: isSignatureField
|
|
? {
|
|
signatureImageAsBase64,
|
|
typedSignature,
|
|
}
|
|
: null,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const directTemplateNonSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature === null);
|
|
|
|
const directTemplateSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature !== null);
|
|
|
|
// Enforce the organisation document-creation limit before creating the document.
|
|
await assertOrganisationRatesAndLimits({
|
|
organisationId: directTemplateEnvelope.team.organisationId,
|
|
type: 'document',
|
|
count: 1,
|
|
});
|
|
|
|
const initialRequestTime = new Date();
|
|
|
|
// Key = original envelope item ID
|
|
// Value = duplicated envelope item ID.
|
|
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
|
|
|
// Duplicate the envelope item data.
|
|
const envelopeItemsToCreate = await Promise.all(
|
|
directTemplateEnvelope.envelopeItems.map(async (item, i) => {
|
|
const buffer = await getFileServerSide(item.documentData);
|
|
|
|
const titleToUse = item.title || directTemplateEnvelope.title;
|
|
|
|
const { documentData: newDocumentData } = await putPdfFileServerSide({
|
|
name: titleToUse,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(buffer),
|
|
});
|
|
|
|
const newEnvelopeItemId = prefixedId('envelope_item');
|
|
|
|
oldEnvelopeItemToNewEnvelopeItemIdMap[item.id] = newEnvelopeItemId;
|
|
|
|
return {
|
|
id: newEnvelopeItemId,
|
|
title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
|
|
documentDataId: newDocumentData.id,
|
|
order: item.order !== undefined ? item.order : i + 1,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const documentMeta = await prisma.documentMeta.create({
|
|
data: derivedDocumentMeta,
|
|
});
|
|
|
|
const incrementedDocumentId = await incrementDocumentId();
|
|
|
|
const { createdEnvelope, recipientId, token } = await prisma.$transaction(async (tx) => {
|
|
// Create the envelope and non direct template recipients.
|
|
const createdEnvelope = await tx.envelope.create({
|
|
data: {
|
|
id: prefixedId('envelope'),
|
|
secondaryId: incrementedDocumentId.formattedDocumentId,
|
|
type: EnvelopeType.DOCUMENT,
|
|
internalVersion: directTemplateEnvelope.internalVersion,
|
|
signatureLevel,
|
|
qrToken: prefixedId('qr'),
|
|
source: DocumentSource.TEMPLATE_DIRECT_LINK,
|
|
templateId: directTemplateEnvelopeLegacyId,
|
|
userId: directTemplateEnvelope.userId,
|
|
teamId: directTemplateEnvelope.teamId,
|
|
title: directTemplateEnvelope.title,
|
|
createdAt: initialRequestTime,
|
|
status: DocumentStatus.PENDING,
|
|
externalId: directTemplateExternalId,
|
|
visibility: settings.documentVisibility,
|
|
envelopeItems: {
|
|
createMany: {
|
|
data: envelopeItemsToCreate,
|
|
},
|
|
},
|
|
authOptions: createDocumentAuthOptions({
|
|
globalAccessAuth: templateAuthOptions.globalAccessAuth,
|
|
globalActionAuth: templateAuthOptions.globalActionAuth,
|
|
}),
|
|
recipients: {
|
|
createMany: {
|
|
data: nonDirectTemplateRecipients.map((recipient) => {
|
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions);
|
|
|
|
return {
|
|
email: recipient.email,
|
|
name: recipient.name,
|
|
role: recipient.role,
|
|
authOptions: createRecipientAuthOptions({
|
|
accessAuth: authOptions.accessAuth,
|
|
actionAuth: authOptions.actionAuth,
|
|
}),
|
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
signingOrder: recipient.signingOrder,
|
|
token: nanoid(),
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
documentMetaId: documentMeta.id,
|
|
},
|
|
include: {
|
|
recipients: true,
|
|
team: {
|
|
select: {
|
|
url: true,
|
|
},
|
|
},
|
|
envelopeItems: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
|
|
|
Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
|
|
const recipient = createdEnvelope.recipients.find((recipient) => recipient.email === templateRecipient.email);
|
|
|
|
if (!recipient) {
|
|
throw new Error('Recipient not found.');
|
|
}
|
|
|
|
nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
|
|
templateRecipient.fields.map((field) => ({
|
|
envelopeId: createdEnvelope.id,
|
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
|
recipientId: recipient.id,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: field.fieldMeta,
|
|
})),
|
|
);
|
|
});
|
|
|
|
await tx.field.createMany({
|
|
data: nonDirectRecipientFieldsToCreate.map((field) => ({
|
|
...field,
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
|
})),
|
|
});
|
|
|
|
// Create the direct recipient and their non signature fields.
|
|
const createdDirectRecipient = await tx.recipient.create({
|
|
data: {
|
|
envelopeId: createdEnvelope.id,
|
|
email: directRecipientEmail,
|
|
name: directRecipientName,
|
|
authOptions: createRecipientAuthOptions({
|
|
accessAuth: directTemplateRecipientAuthOptions.accessAuth,
|
|
actionAuth: directTemplateRecipientAuthOptions.actionAuth,
|
|
}),
|
|
role: directTemplateRecipient.role,
|
|
token: nanoid(),
|
|
signingStatus: SigningStatus.SIGNED,
|
|
sendStatus: SendStatus.SENT,
|
|
signedAt: initialRequestTime,
|
|
signingOrder: directTemplateRecipient.signingOrder,
|
|
fields: {
|
|
createMany: {
|
|
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => {
|
|
let inserted = true;
|
|
|
|
// Custom logic for V2 to only insert if values exist.
|
|
if (directTemplateEnvelope.internalVersion === 2) {
|
|
inserted = customText !== '';
|
|
}
|
|
|
|
return {
|
|
envelopeId: createdEnvelope.id,
|
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
|
type: templateField.type,
|
|
page: templateField.page,
|
|
positionX: templateField.positionX,
|
|
positionY: templateField.positionY,
|
|
width: templateField.width,
|
|
height: templateField.height,
|
|
customText: customText ?? '',
|
|
inserted,
|
|
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
fields: true,
|
|
},
|
|
});
|
|
|
|
// Create any direct recipient signature fields.
|
|
// Note: It's done like this because we can't nest things in createMany.
|
|
const createdDirectRecipientSignatureFields: CreatedDirectRecipientField[] = await Promise.all(
|
|
directTemplateSignatureFields.map(async ({ templateField, signature, derivedRecipientActionAuth }) => {
|
|
if (!signature) {
|
|
throw new Error('Not possible.');
|
|
}
|
|
|
|
const field = await tx.field.create({
|
|
data: {
|
|
envelopeId: createdEnvelope.id,
|
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
|
recipientId: createdDirectRecipient.id,
|
|
type: templateField.type,
|
|
page: templateField.page,
|
|
positionX: templateField.positionX,
|
|
positionY: templateField.positionY,
|
|
width: templateField.width,
|
|
height: templateField.height,
|
|
customText: '',
|
|
inserted: true,
|
|
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
|
signature: {
|
|
create: {
|
|
recipientId: createdDirectRecipient.id,
|
|
signatureImageAsBase64: signature.signatureImageAsBase64,
|
|
typedSignature: signature.typedSignature,
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
signature: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
field,
|
|
derivedRecipientActionAuth,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
|
...createdDirectRecipient.fields.map((field) => ({
|
|
field,
|
|
derivedRecipientActionAuth: undefined,
|
|
})),
|
|
...createdDirectRecipientSignatureFields,
|
|
];
|
|
|
|
/**
|
|
* Create the following audit logs.
|
|
* - DOCUMENT_CREATED
|
|
* - DOCUMENT_FIELD_INSERTED
|
|
* - DOCUMENT_RECIPIENT_COMPLETED
|
|
*/
|
|
const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
title: createdEnvelope.title,
|
|
source: {
|
|
type: DocumentSource.TEMPLATE_DIRECT_LINK,
|
|
templateId: directTemplateEnvelopeLegacyId,
|
|
directRecipientEmail,
|
|
},
|
|
},
|
|
}),
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: createdDirectRecipient.email,
|
|
recipientId: createdDirectRecipient.id,
|
|
recipientName: createdDirectRecipient.name,
|
|
recipientRole: createdDirectRecipient.role,
|
|
accessAuth: derivedRecipientAccessAuth || undefined,
|
|
},
|
|
}),
|
|
...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) =>
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: createdDirectRecipient.email,
|
|
recipientId: createdDirectRecipient.id,
|
|
recipientName: createdDirectRecipient.name,
|
|
recipientRole: createdDirectRecipient.role,
|
|
fieldId: field.secondaryId,
|
|
field: match(field.type)
|
|
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, (type) => ({
|
|
type,
|
|
data: field.signature?.signatureImageAsBase64 || field.signature?.typedSignature || '',
|
|
}))
|
|
.with(
|
|
FieldType.DATE,
|
|
FieldType.EMAIL,
|
|
FieldType.INITIALS,
|
|
FieldType.NAME,
|
|
FieldType.TEXT,
|
|
FieldType.NUMBER,
|
|
FieldType.CHECKBOX,
|
|
FieldType.DROPDOWN,
|
|
FieldType.RADIO,
|
|
(type) => ({
|
|
type,
|
|
data: field.customText,
|
|
}),
|
|
)
|
|
.exhaustive(),
|
|
fieldSecurity: derivedRecipientActionAuth
|
|
? {
|
|
type: derivedRecipientActionAuth,
|
|
}
|
|
: undefined,
|
|
},
|
|
}),
|
|
),
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
|
envelopeId: createdEnvelope.id,
|
|
user: {
|
|
id: user?.id,
|
|
name: user?.name,
|
|
email: directRecipientEmail,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
recipientEmail: createdDirectRecipient.email,
|
|
recipientId: createdDirectRecipient.id,
|
|
recipientName: createdDirectRecipient.name,
|
|
recipientRole: createdDirectRecipient.role,
|
|
actionAuth: createdDirectRecipient.authOptions?.actionAuth ?? [],
|
|
},
|
|
}),
|
|
];
|
|
|
|
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: {
|
|
...(nextSigner && documentMeta?.allowDictateNextSigner
|
|
? {
|
|
name: nextSigner.name,
|
|
email: nextSigner.email,
|
|
}
|
|
: {}),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
await tx.documentAuditLog.createMany({
|
|
data: auditLogsToCreate,
|
|
});
|
|
|
|
const templateAttachments = await tx.envelopeAttachment.findMany({
|
|
where: {
|
|
envelopeId: directTemplateEnvelope.id,
|
|
},
|
|
});
|
|
|
|
if (templateAttachments.length > 0) {
|
|
await tx.envelopeAttachment.createMany({
|
|
data: templateAttachments.map((attachment) => ({
|
|
envelopeId: createdEnvelope.id,
|
|
type: attachment.type,
|
|
label: attachment.label,
|
|
data: attachment.data,
|
|
})),
|
|
});
|
|
}
|
|
|
|
return {
|
|
createdEnvelope,
|
|
token: createdDirectRecipient.token,
|
|
recipientId: createdDirectRecipient.id,
|
|
};
|
|
});
|
|
|
|
const emailSettings = extractDerivedDocumentEmailSettings(documentMeta);
|
|
|
|
if (emailSettings.ownerDocumentCreated) {
|
|
await jobs.triggerJob({
|
|
name: 'send.document.created.from.direct.template.email',
|
|
payload: {
|
|
envelopeId: createdEnvelope.id,
|
|
recipientId,
|
|
},
|
|
});
|
|
}
|
|
|
|
try {
|
|
// This handles sending emails and sealing the document if required.
|
|
await sendDocument({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: createdEnvelope.id,
|
|
},
|
|
userId: createdEnvelope.userId,
|
|
teamId: createdEnvelope.teamId,
|
|
requestMetadata,
|
|
});
|
|
|
|
// Refetch envelope so we get the final data.
|
|
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
|
|
where: {
|
|
id: createdEnvelope.id,
|
|
},
|
|
include: {
|
|
documentMeta: true,
|
|
recipients: true,
|
|
},
|
|
});
|
|
|
|
await triggerWebhook({
|
|
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
|
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
|
|
userId: refetchedEnvelope.userId,
|
|
teamId: refetchedEnvelope.teamId ?? undefined,
|
|
});
|
|
} catch (err) {
|
|
console.error('[CREATE_DOCUMENT_FROM_DIRECT_TEMPLATE]:', err);
|
|
|
|
// Don't launch an error since the document has already been created.
|
|
// Log and reseal as required until we configure middleware.
|
|
}
|
|
|
|
return {
|
|
token,
|
|
envelopeId: createdEnvelope.id,
|
|
documentId: incrementedDocumentId.documentId,
|
|
recipientId,
|
|
};
|
|
};
|