mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
717 lines
19 KiB
TypeScript
717 lines
19 KiB
TypeScript
import type { Envelope } from '@prisma/client';
|
|
import { DocumentDataType, EnvelopeType } from '@prisma/client';
|
|
|
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import { jobs } from '@documenso/lib/jobs/client';
|
|
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
|
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
|
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
|
import { duplicateEnvelope } from '@documenso/lib/server-only/envelope/duplicate-envelope';
|
|
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
|
|
import {
|
|
ZCreateDocumentFromDirectTemplateResponseSchema,
|
|
createDocumentFromDirectTemplate,
|
|
} from '@documenso/lib/server-only/template/create-document-from-direct-template';
|
|
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
|
import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link';
|
|
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
|
import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link';
|
|
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
|
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
|
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
|
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
|
|
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
|
|
import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
|
|
|
|
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
|
|
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
|
|
import {
|
|
ZBulkSendTemplateMutationSchema,
|
|
ZCreateDocumentFromDirectTemplateRequestSchema,
|
|
ZCreateDocumentFromTemplateRequestSchema,
|
|
ZCreateDocumentFromTemplateResponseSchema,
|
|
ZCreateTemplateDirectLinkRequestSchema,
|
|
ZCreateTemplateDirectLinkResponseSchema,
|
|
ZCreateTemplateMutationSchema,
|
|
ZCreateTemplateResponseSchema,
|
|
ZCreateTemplateV2RequestSchema,
|
|
ZCreateTemplateV2ResponseSchema,
|
|
ZDeleteTemplateDirectLinkRequestSchema,
|
|
ZDeleteTemplateMutationSchema,
|
|
ZDuplicateTemplateMutationSchema,
|
|
ZDuplicateTemplateResponseSchema,
|
|
ZFindTemplatesRequestSchema,
|
|
ZFindTemplatesResponseSchema,
|
|
ZGetTemplateByIdRequestSchema,
|
|
ZGetTemplateByIdResponseSchema,
|
|
ZToggleTemplateDirectLinkRequestSchema,
|
|
ZToggleTemplateDirectLinkResponseSchema,
|
|
ZUpdateTemplateRequestSchema,
|
|
ZUpdateTemplateResponseSchema,
|
|
} from './schema';
|
|
|
|
export const templateRouter = router({
|
|
/**
|
|
* @public
|
|
*/
|
|
findTemplates: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'GET',
|
|
path: '/template',
|
|
summary: 'Find templates',
|
|
description: 'Find templates based on a search criteria',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZFindTemplatesRequestSchema)
|
|
.output(ZFindTemplatesResponseSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
folderId: input.folderId,
|
|
},
|
|
});
|
|
|
|
const result = await findTemplates({
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
...input,
|
|
});
|
|
|
|
// Remapping for backwards compatibility.
|
|
return {
|
|
...result,
|
|
data: result.data.map((envelope) => {
|
|
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
|
|
|
return {
|
|
id: legacyTemplateId,
|
|
envelopeId: envelope.id,
|
|
type: envelope.templateType,
|
|
visibility: envelope.visibility,
|
|
externalId: envelope.externalId,
|
|
title: envelope.title,
|
|
userId: envelope.userId,
|
|
teamId: envelope.teamId,
|
|
authOptions: envelope.authOptions,
|
|
createdAt: envelope.createdAt,
|
|
updatedAt: envelope.updatedAt,
|
|
publicTitle: envelope.publicTitle,
|
|
publicDescription: envelope.publicDescription,
|
|
folderId: envelope.folderId,
|
|
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
|
team: envelope.team,
|
|
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
|
recipients: envelope.recipients.map((recipient) =>
|
|
mapRecipientToLegacyRecipient(recipient, envelope),
|
|
),
|
|
templateMeta: envelope.documentMeta,
|
|
directLink: envelope.directLink,
|
|
};
|
|
}),
|
|
};
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
getTemplateById: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'GET',
|
|
path: '/template/{templateId}',
|
|
summary: 'Get template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZGetTemplateByIdRequestSchema)
|
|
.output(ZGetTemplateByIdResponseSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId } = input;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
return await getTemplateById({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
});
|
|
}),
|
|
|
|
/**
|
|
* Wait until RR7 so we can passthrough documents.
|
|
*
|
|
* @private
|
|
*/
|
|
createTemplate: authenticatedProcedure
|
|
// .meta({ // Note before releasing this to public, update the response schema to be correct.
|
|
// openapi: {
|
|
// method: 'POST',
|
|
// path: '/template/create',
|
|
// summary: 'Create template',
|
|
// description: 'Create a new template',
|
|
// tags: ['Template'],
|
|
// },
|
|
// })
|
|
.input(ZCreateTemplateMutationSchema)
|
|
.output(ZCreateTemplateResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { title, templateDocumentDataId, folderId } = input;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
folderId,
|
|
},
|
|
});
|
|
|
|
const envelope = await createEnvelope({
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
internalVersion: 1,
|
|
data: {
|
|
type: EnvelopeType.TEMPLATE,
|
|
title,
|
|
folderId,
|
|
envelopeItems: [
|
|
{
|
|
documentDataId: templateDocumentDataId,
|
|
},
|
|
],
|
|
},
|
|
requestMetadata: ctx.metadata,
|
|
});
|
|
|
|
return {
|
|
legacyTemplateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
|
};
|
|
}),
|
|
|
|
/**
|
|
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
|
|
*
|
|
* @public
|
|
* @deprecated
|
|
*/
|
|
createTemplateTemporary: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/create/beta',
|
|
summary: 'Create template',
|
|
description:
|
|
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZCreateTemplateV2RequestSchema)
|
|
.output(ZCreateTemplateV2ResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId, user } = ctx;
|
|
|
|
const {
|
|
title,
|
|
folderId,
|
|
externalId,
|
|
visibility,
|
|
globalAccessAuth,
|
|
globalActionAuth,
|
|
publicTitle,
|
|
publicDescription,
|
|
type,
|
|
meta,
|
|
} = input;
|
|
|
|
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
|
|
|
|
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
|
|
|
|
const templateDocumentData = await createDocumentData({
|
|
data: key,
|
|
type: DocumentDataType.S3_PATH,
|
|
});
|
|
|
|
const createdTemplate = await createEnvelope({
|
|
userId: user.id,
|
|
teamId,
|
|
internalVersion: 1,
|
|
data: {
|
|
type: EnvelopeType.TEMPLATE,
|
|
title,
|
|
envelopeItems: [
|
|
{
|
|
documentDataId: templateDocumentData.id,
|
|
},
|
|
],
|
|
folderId,
|
|
externalId: externalId ?? undefined,
|
|
visibility,
|
|
globalAccessAuth,
|
|
globalActionAuth,
|
|
templateType: type,
|
|
publicTitle,
|
|
publicDescription,
|
|
},
|
|
meta,
|
|
requestMetadata: ctx.metadata,
|
|
});
|
|
|
|
const legacyTemplateId = mapSecondaryIdToTemplateId(createdTemplate.secondaryId);
|
|
|
|
const fullTemplate = await getTemplateById({
|
|
id: {
|
|
type: 'templateId',
|
|
id: legacyTemplateId,
|
|
},
|
|
userId: user.id,
|
|
teamId,
|
|
});
|
|
|
|
return {
|
|
template: fullTemplate,
|
|
uploadUrl: url,
|
|
};
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
updateTemplate: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/update',
|
|
summary: 'Update template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZUpdateTemplateRequestSchema)
|
|
.output(ZUpdateTemplateResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId, data, meta } = input;
|
|
const userId = ctx.user.id;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
const envelope = await updateEnvelope({
|
|
userId,
|
|
teamId,
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
data: {
|
|
...data,
|
|
templateType: data?.type, // Backwards compatibility.
|
|
},
|
|
meta,
|
|
requestMetadata: ctx.metadata,
|
|
});
|
|
|
|
return mapEnvelopeToTemplateLite(envelope);
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
duplicateTemplate: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/duplicate',
|
|
summary: 'Duplicate template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZDuplicateTemplateMutationSchema)
|
|
.output(ZDuplicateTemplateResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId } = input;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
const duplicatedEnvelope = await duplicateEnvelope({
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
});
|
|
|
|
return mapEnvelopeToTemplateLite(duplicatedEnvelope.envelope);
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
deleteTemplate: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/delete',
|
|
summary: 'Delete template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZDeleteTemplateMutationSchema)
|
|
.output(ZSuccessResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId } = input;
|
|
const userId = ctx.user.id;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
await deleteTemplate({
|
|
userId,
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
teamId,
|
|
});
|
|
|
|
return ZGenericSuccessResponse;
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
createDocumentFromTemplate: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/use',
|
|
summary: 'Use template',
|
|
description: 'Use the template to create a document',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZCreateDocumentFromTemplateRequestSchema)
|
|
.output(ZCreateDocumentFromTemplateResponseSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const { teamId } = ctx;
|
|
const {
|
|
templateId,
|
|
recipients,
|
|
distributeDocument,
|
|
customDocumentDataId,
|
|
prefillFields,
|
|
folderId,
|
|
} = input;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
const limits = await getServerLimits({ userId: ctx.user.id, teamId });
|
|
|
|
if (limits.remaining.documents === 0) {
|
|
throw new Error('You have reached your document limit.');
|
|
}
|
|
|
|
// Backwards compatibility mapping since we need the envelopeItemId for the custom document data.
|
|
const customDocumentData = customDocumentDataId
|
|
? [
|
|
{
|
|
documentDataId: customDocumentDataId,
|
|
envelopeItemId: undefined,
|
|
},
|
|
]
|
|
: input.customDocumentData || [];
|
|
|
|
const envelope: Envelope = await createDocumentFromTemplate({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
teamId,
|
|
userId: ctx.user.id,
|
|
recipients,
|
|
customDocumentData,
|
|
requestMetadata: ctx.metadata,
|
|
folderId,
|
|
prefillFields,
|
|
});
|
|
|
|
if (distributeDocument) {
|
|
await sendDocument({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: envelope.id,
|
|
},
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
requestMetadata: ctx.metadata,
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
|
|
throw new AppError('DOCUMENT_SEND_FAILED');
|
|
});
|
|
}
|
|
|
|
return getDocumentWithDetailsById({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: envelope.id,
|
|
},
|
|
userId: ctx.user.id,
|
|
teamId,
|
|
});
|
|
}),
|
|
|
|
/**
|
|
* Leaving this endpoint as private for now until there is a use case for it.
|
|
*
|
|
* @private
|
|
*/
|
|
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
|
|
// .meta({
|
|
// openapi: {
|
|
// method: 'POST',
|
|
// path: '/template/direct/use',
|
|
// summary: 'Use direct template',
|
|
// description: 'Use a direct template to create a document',
|
|
// tags: ['Template'],
|
|
// },
|
|
// })
|
|
.input(ZCreateDocumentFromDirectTemplateRequestSchema)
|
|
.output(ZCreateDocumentFromDirectTemplateResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const {
|
|
directRecipientName,
|
|
directRecipientEmail,
|
|
directTemplateToken,
|
|
directTemplateExternalId,
|
|
signedFieldValues,
|
|
templateUpdatedAt,
|
|
} = input;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
directTemplateToken,
|
|
},
|
|
});
|
|
|
|
return await createDocumentFromDirectTemplate({
|
|
directRecipientName,
|
|
directRecipientEmail,
|
|
directTemplateToken,
|
|
directTemplateExternalId,
|
|
signedFieldValues,
|
|
templateUpdatedAt,
|
|
user: ctx.user
|
|
? {
|
|
id: ctx.user.id,
|
|
name: ctx.user.name || undefined,
|
|
email: ctx.user.email,
|
|
}
|
|
: undefined,
|
|
requestMetadata: ctx.metadata,
|
|
});
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
createTemplateDirectLink: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/direct/create',
|
|
summary: 'Create direct link',
|
|
description: 'Create a direct link for a template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZCreateTemplateDirectLinkRequestSchema)
|
|
.output(ZCreateTemplateDirectLinkResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId, directRecipientId } = input;
|
|
|
|
const userId = ctx.user.id;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
directRecipientId,
|
|
},
|
|
});
|
|
|
|
const template = await getTemplateById({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
teamId,
|
|
userId: ctx.user.id,
|
|
});
|
|
|
|
const limits = await getServerLimits({ userId: ctx.user.id, teamId: template.teamId });
|
|
|
|
if (limits.remaining.directTemplates === 0) {
|
|
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
|
message: 'You have reached your direct templates limit.',
|
|
});
|
|
}
|
|
|
|
return await createTemplateDirectLink({
|
|
userId,
|
|
teamId,
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
directRecipientId,
|
|
});
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
deleteTemplateDirectLink: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/direct/delete',
|
|
summary: 'Delete direct link',
|
|
description: 'Delete a direct link for a template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZDeleteTemplateDirectLinkRequestSchema)
|
|
.output(ZSuccessResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId } = input;
|
|
|
|
const userId = ctx.user.id;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
await deleteTemplateDirectLink({ userId, teamId, templateId });
|
|
|
|
return ZGenericSuccessResponse;
|
|
}),
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
toggleTemplateDirectLink: authenticatedProcedure
|
|
.meta({
|
|
openapi: {
|
|
method: 'POST',
|
|
path: '/template/direct/toggle',
|
|
summary: 'Toggle direct link',
|
|
description: 'Enable or disable a direct link for a template',
|
|
tags: ['Template'],
|
|
},
|
|
})
|
|
.input(ZToggleTemplateDirectLinkRequestSchema)
|
|
.output(ZToggleTemplateDirectLinkResponseSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { teamId } = ctx;
|
|
const { templateId, enabled } = input;
|
|
|
|
const userId = ctx.user.id;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
},
|
|
});
|
|
|
|
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
|
|
}),
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
uploadBulkSend: authenticatedProcedure
|
|
.input(ZBulkSendTemplateMutationSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const { templateId, teamId, csv, sendImmediately } = input;
|
|
const { user } = ctx;
|
|
|
|
ctx.logger.info({
|
|
input: {
|
|
templateId,
|
|
teamId,
|
|
},
|
|
});
|
|
|
|
if (csv.length > 4 * 1024 * 1024) {
|
|
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
|
message: 'File size exceeds 4MB limit',
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
const template = await getTemplateById({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
teamId,
|
|
userId: user.id,
|
|
});
|
|
|
|
if (!template) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Template not found',
|
|
});
|
|
}
|
|
|
|
await jobs.triggerJob({
|
|
name: 'internal.bulk-send-template',
|
|
payload: {
|
|
userId: user.id,
|
|
teamId,
|
|
templateId,
|
|
csvContent: csv,
|
|
sendImmediately,
|
|
requestMetadata: ctx.metadata.requestMetadata,
|
|
},
|
|
});
|
|
|
|
return { success: true };
|
|
}),
|
|
});
|