mirror of
https://github.com/documenso/documenso.git
synced 2025-11-24 21:51:40 +10:00
fix: merge conflicts
This commit is contained in:
@ -1,6 +1,5 @@
|
||||
import { createNextRoute } from '@ts-rest/next';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
@ -16,8 +15,7 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
|
||||
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
|
||||
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
|
||||
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
|
||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||
@ -26,11 +24,10 @@ import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-for
|
||||
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
|
||||
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
||||
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
|
||||
import type { TCreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
@ -54,6 +51,7 @@ import {
|
||||
} from '@documenso/lib/universal/upload/server-actions';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentDataType,
|
||||
DocumentStatus,
|
||||
@ -63,7 +61,6 @@ import {
|
||||
|
||||
import { ApiContractV1 } from './contract';
|
||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||
import { ZTemplateWithDataSchema } from './schema';
|
||||
|
||||
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
||||
@ -98,13 +95,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
teamId: team?.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const fields = await getFieldsForDocument({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const parsedMetaFields = fields.map((field) => {
|
||||
@ -209,7 +207,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
|
||||
try {
|
||||
@ -232,6 +230,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
id: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -248,7 +247,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { body } = args;
|
||||
|
||||
try {
|
||||
@ -316,12 +315,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
formValues: body.formValues,
|
||||
documentDataId: documentData.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
subject: body.meta.subject,
|
||||
message: body.meta.message,
|
||||
timezone,
|
||||
@ -332,7 +332,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
||||
distributionMethod: body.meta.distributionMethod,
|
||||
emailSettings: body.meta.emailSettings,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
if (body.authOptions) {
|
||||
@ -343,16 +343,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
data: {
|
||||
...body.authOptions,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
const { recipients } = await setRecipientsForDocument({
|
||||
const { recipients } = await setDocumentRecipients({
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
documentId: document.id,
|
||||
recipients: body.recipients,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -416,11 +416,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const parsed = ZTemplateWithDataSchema.parse(template);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: parsed,
|
||||
body: {
|
||||
...template,
|
||||
Field: template.fields.map((field) => ({
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
|
||||
})),
|
||||
Recipient: template.recipients,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return AppError.toRestAPIError(err);
|
||||
@ -439,12 +444,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const parsed = z.array(ZTemplateWithDataSchema).parse(templates);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
templates: parsed,
|
||||
templates: templates.map((template) => ({
|
||||
...template,
|
||||
Field: template.fields.map((field) => ({
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
|
||||
})),
|
||||
Recipient: template.recipients,
|
||||
})),
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
@ -453,7 +463,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { body, params } = args;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||
@ -517,8 +527,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
...body.meta,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -528,7 +539,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -536,7 +547,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
status: 200,
|
||||
body: {
|
||||
documentId: document.id,
|
||||
recipients: document.Recipient.map((recipient) => ({
|
||||
recipients: document.recipients.map((recipient) => ({
|
||||
recipientId: recipient.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -550,7 +561,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { body, params } = args;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
|
||||
@ -566,7 +577,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
const templateId = Number(params.templateId);
|
||||
|
||||
let document: TCreateDocumentFromTemplateResponse | null = null;
|
||||
let document: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
|
||||
|
||||
try {
|
||||
document = await createDocumentFromTemplate({
|
||||
@ -579,6 +590,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
title: body.title,
|
||||
...body.meta,
|
||||
},
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
} catch (err) {
|
||||
return AppError.toRestAPIError(err);
|
||||
@ -621,7 +633,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: body.authOptions,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@ -629,7 +641,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
status: 200,
|
||||
body: {
|
||||
documentId: document.id,
|
||||
recipients: document.Recipient.map((recipient) => ({
|
||||
recipients: document.recipients.map((recipient) => ({
|
||||
recipientId: recipient.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -642,7 +654,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { sendEmail, sendCompletionEmails } = args.body;
|
||||
|
||||
@ -678,21 +690,22 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
emailSettings: {
|
||||
...emailSettings,
|
||||
documentCompleted: sendCompletionEmails,
|
||||
ownerDocumentCompleted: sendCompletionEmails,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
}
|
||||
|
||||
const { Recipient: recipients, ...sentDocument } = await sendDocument({
|
||||
const { recipients, ...sentDocument } = await sendDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
sendEmail,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -716,7 +729,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
resendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { recipients } = args.body;
|
||||
|
||||
@ -726,7 +739,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
documentId: Number(documentId),
|
||||
recipients,
|
||||
teamId: team?.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -745,7 +758,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { name, email, role, authOptions, signingOrder } = args.body;
|
||||
|
||||
@ -791,7 +804,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
|
||||
try {
|
||||
const { recipients: newRecipients } = await setRecipientsForDocument({
|
||||
const { recipients: newRecipients } = await setDocumentRecipients({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
@ -809,7 +822,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
actionAuth: authOptions?.actionAuth ?? null,
|
||||
},
|
||||
],
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
requestMetadata: metadata,
|
||||
});
|
||||
|
||||
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
||||
@ -1040,12 +1053,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
|
||||
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
|
||||
success: true,
|
||||
data: {},
|
||||
data: undefined,
|
||||
}))
|
||||
.with('FREE_SIGNATURE', () => ({
|
||||
success: false,
|
||||
error: 'FREE_SIGNATURE is not supported',
|
||||
data: {},
|
||||
data: undefined,
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
@ -1068,7 +1081,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
fieldMeta: result.data,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1083,7 +1096,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId,
|
||||
fieldType: field.type,
|
||||
},
|
||||
@ -1102,7 +1115,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
pageWidth: Number(field.width),
|
||||
pageHeight: Number(field.height),
|
||||
customText: field.customText,
|
||||
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta),
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
inserted: field.inserted,
|
||||
};
|
||||
}),
|
||||
@ -1574,3 +1587,39 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const updateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
data,
|
||||
}: {
|
||||
documentId: number;
|
||||
data: Prisma.DocumentUpdateInput;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}) => {
|
||||
return await prisma.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,6 +2,8 @@ import type { NextApiRequest } from 'next';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { Team, User } from '@documenso/prisma/client';
|
||||
|
||||
export const authenticatedMiddleware = <
|
||||
@ -13,7 +15,12 @@ export const authenticatedMiddleware = <
|
||||
body: unknown;
|
||||
},
|
||||
>(
|
||||
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
|
||||
handler: (
|
||||
args: T,
|
||||
user: User,
|
||||
team: Team | null | undefined,
|
||||
options: { metadata: ApiRequestMetadata },
|
||||
) => Promise<R>,
|
||||
) => {
|
||||
return async (args: T) => {
|
||||
try {
|
||||
@ -36,7 +43,18 @@ export const authenticatedMiddleware = <
|
||||
});
|
||||
}
|
||||
|
||||
return await handler(args, apiToken.user, apiToken.team);
|
||||
const metadata: ApiRequestMetadata = {
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
source: 'apiV1',
|
||||
auth: 'api',
|
||||
auditUser: {
|
||||
id: apiToken.team ? null : apiToken.user.id,
|
||||
email: apiToken.team ? null : apiToken.user.email,
|
||||
name: apiToken.team?.name ?? apiToken.user.name,
|
||||
},
|
||||
};
|
||||
|
||||
return await handler(args, apiToken.user, apiToken.team, { metadata });
|
||||
} catch (err) {
|
||||
console.log({ err: err });
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ test('[DOCUMENT_AUTH]: should allow signing when no auth setup', async ({ page }
|
||||
|
||||
// Check that both are granted access.
|
||||
for (const recipient of recipients) {
|
||||
const { token, Field } = recipient;
|
||||
const { token, fields } = recipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
@ -45,7 +45,7 @@ test('[DOCUMENT_AUTH]: should allow signing when no auth setup', async ({ page }
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
@ -80,7 +80,7 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
||||
|
||||
const recipient = recipients[0];
|
||||
|
||||
const { token, Field } = recipient;
|
||||
const { token, fields } = recipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
@ -102,7 +102,7 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
@ -170,12 +170,12 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
|
||||
|
||||
// Check that both are denied access.
|
||||
for (const recipient of recipients) {
|
||||
const { token, Field } = recipient;
|
||||
const { token, fields } = recipient;
|
||||
|
||||
await page.goto(`/sign/${token}`);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
if (field.type !== FieldType.SIGNATURE) {
|
||||
continue;
|
||||
}
|
||||
@ -229,7 +229,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
});
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const { token, Field } = recipient;
|
||||
const { token, fields } = recipient;
|
||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
// This document has no global action auth, so only account should require auth.
|
||||
@ -241,7 +241,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
if (isAuthRequired) {
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
if (field.type !== FieldType.SIGNATURE) {
|
||||
continue;
|
||||
}
|
||||
@ -271,7 +271,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
@ -340,7 +340,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
});
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const { token, Field } = recipient;
|
||||
const { token, fields } = recipient;
|
||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
// This document HAS global action auth, so account and inherit should require auth.
|
||||
@ -352,7 +352,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
if (isAuthRequired) {
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
if (field.type !== FieldType.SIGNATURE) {
|
||||
continue;
|
||||
}
|
||||
@ -382,7 +382,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
|
||||
@ -151,6 +151,7 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ import { apiSignin } from '../fixtures/authentication';
|
||||
const getDocumentByToken = async (token: string) => {
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
@ -357,7 +357,7 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
|
||||
});
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const { token, Field, role } = recipient;
|
||||
const { token, fields, role } = recipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
@ -378,7 +378,7 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of Field) {
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
@ -479,8 +479,8 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
|
||||
fields: [FieldType.DATE],
|
||||
});
|
||||
|
||||
const { token, Field } = recipients[0];
|
||||
const [recipientField] = Field;
|
||||
const { token, fields } = recipients[0];
|
||||
const [recipientField] = fields;
|
||||
|
||||
await page.goto(`/sign/${token}`);
|
||||
await page.waitForURL(`/sign/${token}`);
|
||||
@ -496,7 +496,7 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn
|
||||
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipient: {
|
||||
email: 'user1@example.com',
|
||||
},
|
||||
documentId: Number(document.id),
|
||||
@ -580,14 +580,14 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
||||
|
||||
const createdDocument = await prisma.document.findFirst({
|
||||
where: { title: documentTitle },
|
||||
include: { Recipient: true },
|
||||
include: { recipients: true },
|
||||
});
|
||||
|
||||
expect(createdDocument).not.toBeNull();
|
||||
expect(createdDocument?.Recipient.length).toBe(3);
|
||||
expect(createdDocument?.recipients.length).toBe(3);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const recipient = createdDocument?.Recipient.find(
|
||||
const recipient = createdDocument?.recipients.find(
|
||||
(r) => r.email === `user${i + 1}@example.com`,
|
||||
);
|
||||
expect(recipient).not.toBeNull();
|
||||
|
||||
@ -129,7 +129,7 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients'
|
||||
// signout
|
||||
await apiSignout({ page });
|
||||
|
||||
for (const recipient of pendingDocument.Recipient) {
|
||||
for (const recipient of pendingDocument.recipients) {
|
||||
await apiSignin({
|
||||
page,
|
||||
email: recipient.email,
|
||||
|
||||
@ -45,7 +45,7 @@ test.describe('Signing Certificate Tests', () => {
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of recipient.Field) {
|
||||
for (const field of recipient.fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
@ -122,7 +122,7 @@ test.describe('Signing Certificate Tests', () => {
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of recipient.Field) {
|
||||
for (const field of recipient.fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
@ -199,7 +199,7 @@ test.describe('Signing Certificate Tests', () => {
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
for (const field of recipient.Field) {
|
||||
for (const field of recipient.fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
@ -27,7 +27,7 @@ function createTempPdfFile() {
|
||||
'%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000052 00000 n\n0000000101 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n178\n%%EOF',
|
||||
);
|
||||
|
||||
fs.writeFileSync(tempFilePath, pdfContent);
|
||||
fs.writeFileSync(tempFilePath, new Uint8Array(pdfContent));
|
||||
return tempFilePath;
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
@ -144,8 +144,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
||||
expect(document.documentMeta?.subject).toEqual('SUBJECT');
|
||||
expect(document.documentMeta?.timezone).toEqual('Etc/UTC');
|
||||
|
||||
const recipientOne = document.Recipient[0];
|
||||
const recipientTwo = document.Recipient[1];
|
||||
const recipientOne = document.recipients[0];
|
||||
const recipientTwo = document.recipients[1];
|
||||
|
||||
const recipientOneAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
@ -259,7 +259,7 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
@ -281,8 +281,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
||||
expect(document.documentMeta?.subject).toEqual('SUBJECT');
|
||||
expect(document.documentMeta?.timezone).toEqual('Etc/UTC');
|
||||
|
||||
const recipientOne = document.Recipient[0];
|
||||
const recipientTwo = document.Recipient[1];
|
||||
const recipientOne = document.recipients[0];
|
||||
const recipientTwo = document.recipients[1];
|
||||
|
||||
const recipientOneAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
|
||||
@ -260,7 +260,7 @@ test('[DIRECT_TEMPLATES]: use direct template link with 2 recipients', async ({
|
||||
const secondRecipient = await seedUser();
|
||||
|
||||
const createTemplateOptions = {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.18.1",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/node": "^20",
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/web": "*",
|
||||
|
||||
@ -41,7 +41,10 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
This file list all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
||||
This file lists all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
|
||||
Copyright (c) 2023 Documenso, Inc
|
||||
|
||||
- The Stripe Billing Module
|
||||
|
||||
@ -43,7 +43,7 @@ const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
|
||||
email,
|
||||
},
|
||||
include: {
|
||||
Subscription: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -54,7 +54,7 @@ const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
|
||||
let quota = structuredClone(FREE_PLAN_LIMITS);
|
||||
let remaining = structuredClone(FREE_PLAN_LIMITS);
|
||||
|
||||
const activeSubscriptions = user.Subscription.filter(
|
||||
const activeSubscriptions = user.subscriptions.filter(
|
||||
({ status }) => status === SubscriptionStatus.ACTIVE,
|
||||
);
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ type TransferStripeSubscriptionOptions = {
|
||||
/**
|
||||
* The user to transfer the subscription to.
|
||||
*/
|
||||
user: User & { Subscription: Subscription[] };
|
||||
user: User & { subscriptions: Subscription[] };
|
||||
|
||||
/**
|
||||
* The team the subscription is associated with.
|
||||
@ -54,7 +54,7 @@ export const transferTeamSubscription = async ({
|
||||
]);
|
||||
|
||||
const teamSubscriptionRequired = !subscriptionsContainsActivePlan(
|
||||
user.Subscription,
|
||||
user.subscriptions,
|
||||
teamRelatedPlanPriceIds,
|
||||
);
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/creat
|
||||
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { onEarlyAdoptersCheckout } from './on-early-adopters-checkout';
|
||||
import { onSubscriptionDeleted } from './on-subscription-deleted';
|
||||
import { onSubscriptionUpdated } from './on-subscription-updated';
|
||||
|
||||
@ -56,10 +55,6 @@ export const stripeWebhookHandler = async (
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
if (session.metadata?.source === 'marketing') {
|
||||
await onEarlyAdoptersCheckout({ session });
|
||||
}
|
||||
|
||||
const customerId =
|
||||
typeof session.customer === 'string' ? session.customer : session.customer?.id;
|
||||
|
||||
|
||||
@ -1,140 +0,0 @@
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
||||
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
||||
import { redis } from '@documenso/lib/server-only/redis';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { alphaid, nanoid } from '@documenso/lib/universal/id';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
ReadStatus,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { ZEarlyAdopterCheckoutMetadataSchema } from './early-adopter-checkout-metadata';
|
||||
|
||||
export type OnEarlyAdoptersCheckoutOptions = {
|
||||
session: Stripe.Checkout.Session;
|
||||
};
|
||||
|
||||
export const onEarlyAdoptersCheckout = async ({ session }: OnEarlyAdoptersCheckoutOptions) => {
|
||||
try {
|
||||
const safeMetadata = ZEarlyAdopterCheckoutMetadataSchema.safeParse(session.metadata);
|
||||
|
||||
if (!safeMetadata.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, name, signatureText, signatureDataUrl: signatureDataUrlRef } = safeMetadata.data;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPassword = nanoid(12);
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
email: email.toLowerCase(),
|
||||
password: hashSync(tempPassword),
|
||||
signature: signatureDataUrlRef,
|
||||
},
|
||||
});
|
||||
|
||||
const customerId =
|
||||
typeof session.customer === 'string' ? session.customer : session.customer?.id;
|
||||
|
||||
if (customerId) {
|
||||
await stripe.customers.update(customerId, {
|
||||
metadata: {
|
||||
userId: newUser.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await redis.set(`user:${newUser.id}:temp-password`, tempPassword, {
|
||||
// expire in 1 week
|
||||
ex: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
const signatureDataUrl = await redis.get<string>(`signature:${session.client_reference_id}`);
|
||||
|
||||
const documentBuffer = await fetch(
|
||||
new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url),
|
||||
).then(async (res) => res.arrayBuffer());
|
||||
|
||||
const { id: documentDataId } = await putPdfFile({
|
||||
name: 'Documenso Supporter Pledge.pdf',
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(documentBuffer),
|
||||
});
|
||||
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
title: 'Documenso Supporter Pledge.pdf',
|
||||
status: DocumentStatus.COMPLETED,
|
||||
userId: newUser.id,
|
||||
documentDataId,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = await prisma.recipient.create({
|
||||
data: {
|
||||
name,
|
||||
email: email.toLowerCase(),
|
||||
token: alphaid(),
|
||||
readStatus: ReadStatus.OPENED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: new Date(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
type: FieldType.SIGNATURE,
|
||||
recipientId: recipient.id,
|
||||
documentId: document.id,
|
||||
page: 1,
|
||||
positionX: 12.2781,
|
||||
positionY: 81.5789,
|
||||
height: 6.8649,
|
||||
width: 29.5857,
|
||||
inserted: true,
|
||||
customText: '',
|
||||
|
||||
Signature: {
|
||||
create: {
|
||||
typedSignature: signatureDataUrl ? null : signatureText || name,
|
||||
signatureImageAsBase64: signatureDataUrl,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await sealDocument({
|
||||
documentId: document.id,
|
||||
sendEmail: false,
|
||||
});
|
||||
} catch (error) {
|
||||
// We don't want to break the checkout process if something goes wrong here.
|
||||
// This is an additive experience for early adopters, breaking their ability
|
||||
// join would be far worse than not having a signed pledge.
|
||||
console.error('early-supporter-error', error);
|
||||
}
|
||||
};
|
||||
@ -35,12 +35,12 @@ export const isUserEnterprise = async ({
|
||||
select: {
|
||||
owner: {
|
||||
include: {
|
||||
Subscription: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((team) => team.owner.Subscription);
|
||||
.then((team) => team.owner.subscriptions);
|
||||
} else {
|
||||
subscriptions = await prisma.user
|
||||
.findFirstOrThrow({
|
||||
@ -48,10 +48,10 @@ export const isUserEnterprise = async ({
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
Subscription: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
})
|
||||
.then((user) => user.Subscription);
|
||||
.then((user) => user.subscriptions);
|
||||
}
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
|
||||
@ -32,12 +32,12 @@ export const isDocumentPlatform = async ({
|
||||
select: {
|
||||
owner: {
|
||||
include: {
|
||||
Subscription: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((team) => team.owner.Subscription);
|
||||
.then((team) => team.owner.subscriptions);
|
||||
} else {
|
||||
subscriptions = await prisma.user
|
||||
.findFirstOrThrow({
|
||||
@ -45,10 +45,10 @@ export const isDocumentPlatform = async ({
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
Subscription: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
})
|
||||
.then((user) => user.Subscription);
|
||||
.then((user) => user.subscriptions);
|
||||
}
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
|
||||
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { match } from 'ts-pattern';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
@ -40,11 +40,9 @@ export const TemplateDocumentInvite = ({
|
||||
|
||||
const rejectDocumentLink = useMemo(() => {
|
||||
const url = new URL(signDocumentLink);
|
||||
|
||||
url.searchParams.set('reject', 'true');
|
||||
|
||||
return url.toString();
|
||||
}, []);
|
||||
}, [signDocumentLink]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -52,31 +50,32 @@ export const TemplateDocumentInvite = ({
|
||||
|
||||
<Section>
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
{selfSigner ? (
|
||||
<Trans>
|
||||
Please {_(actionVerb).toLowerCase()} your document
|
||||
<br />"{documentName}"
|
||||
</Trans>
|
||||
) : isTeamInvite ? (
|
||||
<>
|
||||
{includeSenderDetails ? (
|
||||
<Trans>
|
||||
{inviterName} on behalf of "{teamName}" has invited you to{' '}
|
||||
{_(actionVerb).toLowerCase()}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{teamName} has invited you to {_(actionVerb).toLowerCase()}
|
||||
</Trans>
|
||||
)}
|
||||
<br />"{documentName}"
|
||||
</>
|
||||
) : (
|
||||
<Trans>
|
||||
{inviterName} has invited you to {_(actionVerb).toLowerCase()}
|
||||
<br />"{documentName}"
|
||||
</Trans>
|
||||
)}
|
||||
{match({ selfSigner, isTeamInvite, includeSenderDetails, teamName })
|
||||
.with({ selfSigner: true }, () => (
|
||||
<Trans>
|
||||
Please {_(actionVerb).toLowerCase()} your document
|
||||
<br />"{documentName}"
|
||||
</Trans>
|
||||
))
|
||||
.with({ isTeamInvite: true, includeSenderDetails: true, teamName: P.string }, () => (
|
||||
<Trans>
|
||||
{inviterName} on behalf of "{teamName}" has invited you to{' '}
|
||||
{_(actionVerb).toLowerCase()}
|
||||
<br />"{documentName}"
|
||||
</Trans>
|
||||
))
|
||||
.with({ isTeamInvite: true, teamName: P.string }, () => (
|
||||
<Trans>
|
||||
{teamName} has invited you to {_(actionVerb).toLowerCase()}
|
||||
<br />"{documentName}"
|
||||
</Trans>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<Trans>
|
||||
{inviterName} has invited you to {_(actionVerb).toLowerCase()}
|
||||
<br />"{documentName}"
|
||||
</Trans>
|
||||
))}
|
||||
</Text>
|
||||
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
|
||||
91
packages/email/templates/bulk-send-complete.tsx
Normal file
91
packages/email/templates/bulk-send-complete.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { Body, Container, Head, Html, Preview, Section, Text } from '../components';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export interface BulkSendCompleteEmailProps {
|
||||
userName: string;
|
||||
templateName: string;
|
||||
totalProcessed: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
errors: string[];
|
||||
assetBaseUrl?: string;
|
||||
}
|
||||
|
||||
export const BulkSendCompleteEmail = ({
|
||||
userName,
|
||||
templateName,
|
||||
totalProcessed,
|
||||
successCount,
|
||||
failedCount,
|
||||
errors,
|
||||
}: BulkSendCompleteEmailProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{_(msg`Bulk send operation complete for template "${templateName}"`)}</Preview>
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Section>
|
||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
||||
<Section>
|
||||
<Text className="text-sm">
|
||||
<Trans>Hi {userName},</Trans>
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm">
|
||||
<Trans>Your bulk send operation for template "{templateName}" has completed.</Trans>
|
||||
</Text>
|
||||
|
||||
<Text className="text-lg font-semibold">
|
||||
<Trans>Summary:</Trans>
|
||||
</Text>
|
||||
|
||||
<ul className="my-2 ml-4 list-inside list-disc">
|
||||
<li>
|
||||
<Trans>Total rows processed: {totalProcessed}</Trans>
|
||||
</li>
|
||||
<li className="mt-1">
|
||||
<Trans>Successfully created: {successCount}</Trans>
|
||||
</li>
|
||||
<li className="mt-1">
|
||||
<Trans>Failed: {failedCount}</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{failedCount > 0 && (
|
||||
<Section className="mt-4">
|
||||
<Text className="text-lg font-semibold">
|
||||
<Trans>The following errors occurred:</Trans>
|
||||
</Text>
|
||||
|
||||
<ul className="my-2 ml-4 list-inside list-disc">
|
||||
{errors.map((error, index) => (
|
||||
<li key={index} className="text-destructive mt-1 text-sm text-slate-400">
|
||||
{error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Text className="text-sm">
|
||||
<Trans>
|
||||
You can view the created documents in your dashboard under the "Documents created
|
||||
from template" section.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
<Container className="mx-auto max-w-xl">
|
||||
<TemplateFooter isDocument={false} />
|
||||
</Container>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
@ -15,6 +15,6 @@
|
||||
"eslint-plugin-package-json": "^0.10.4",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ export type UseCopyShareLinkOptions = {
|
||||
export function useCopyShareLink({ onSuccess, onError }: UseCopyShareLinkOptions) {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } =
|
||||
const { mutateAsync: createOrGetShareLink, isPending: isCreatingShareLink } =
|
||||
trpc.shareLink.createOrGetShareLink.useMutation();
|
||||
|
||||
/**
|
||||
|
||||
@ -4,6 +4,19 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from './time-zones';
|
||||
|
||||
export const DEFAULT_DOCUMENT_DATE_FORMAT = 'yyyy-MM-dd hh:mm a';
|
||||
|
||||
export const VALID_DATE_FORMAT_VALUES = [
|
||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
'yyyy-MM-dd',
|
||||
'dd/MM/yyyy hh:mm a',
|
||||
'MM/dd/yyyy hh:mm a',
|
||||
'yyyy-MM-dd HH:mm',
|
||||
'yy-MM-dd hh:mm a',
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
'MMMM dd, yyyy hh:mm a',
|
||||
'EEEE, MMMM dd, yyyy hh:mm a',
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
] as const;
|
||||
|
||||
export const DATE_FORMATS = [
|
||||
{
|
||||
key: 'yyyy-MM-dd_hh:mm_a',
|
||||
@ -55,7 +68,11 @@ export const DATE_FORMATS = [
|
||||
label: 'ISO 8601',
|
||||
value: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
},
|
||||
];
|
||||
] satisfies {
|
||||
key: string;
|
||||
label: string;
|
||||
value: (typeof VALID_DATE_FORMAT_VALUES)[number];
|
||||
}[];
|
||||
|
||||
export const convertToLocalSystemFormat = (
|
||||
customText: string,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr', 'es'] as const;
|
||||
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr', 'es', 'it', 'pl'] as const;
|
||||
|
||||
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
|
||||
|
||||
@ -46,6 +46,14 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
|
||||
full: 'Spanish',
|
||||
short: 'es',
|
||||
},
|
||||
it: {
|
||||
full: 'Italian',
|
||||
short: 'it',
|
||||
},
|
||||
pl: {
|
||||
short: 'pl',
|
||||
full: 'Polish',
|
||||
},
|
||||
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
|
||||
|
||||
export const isValidLanguageCode = (code: unknown): code is SupportedLanguageCodes =>
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import type { TRPCError } from '@trpc/server';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
|
||||
/**
|
||||
* Generic application error codes.
|
||||
*/
|
||||
@ -24,24 +21,22 @@ export enum AppErrorCode {
|
||||
'PREMIUM_PROFILE_URL' = 'PREMIUM_PROFILE_URL',
|
||||
}
|
||||
|
||||
export const genericErrorCodeToTrpcErrorCodeMap: Record<
|
||||
string,
|
||||
{ code: TRPCError['code']; status: number }
|
||||
> = {
|
||||
[AppErrorCode.ALREADY_EXISTS]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.EXPIRED_CODE]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.INVALID_BODY]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.INVALID_REQUEST]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.NOT_FOUND]: { code: 'NOT_FOUND', status: 404 },
|
||||
[AppErrorCode.NOT_SETUP]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.UNAUTHORIZED]: { code: 'UNAUTHORIZED', status: 401 },
|
||||
[AppErrorCode.UNKNOWN_ERROR]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
|
||||
[AppErrorCode.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
|
||||
};
|
||||
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> =
|
||||
{
|
||||
[AppErrorCode.ALREADY_EXISTS]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.EXPIRED_CODE]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.INVALID_BODY]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.INVALID_REQUEST]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.NOT_FOUND]: { code: 'NOT_FOUND', status: 404 },
|
||||
[AppErrorCode.NOT_SETUP]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.UNAUTHORIZED]: { code: 'UNAUTHORIZED', status: 401 },
|
||||
[AppErrorCode.UNKNOWN_ERROR]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
|
||||
[AppErrorCode.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
|
||||
};
|
||||
|
||||
export const ZAppErrorJsonSchema = z.object({
|
||||
code: z.string(),
|
||||
@ -87,6 +82,8 @@ export class AppError extends Error {
|
||||
*/
|
||||
statusCode?: number;
|
||||
|
||||
name = 'AppError';
|
||||
|
||||
/**
|
||||
* Create a new AppError.
|
||||
*
|
||||
@ -107,17 +104,18 @@ export class AppError extends Error {
|
||||
*
|
||||
* @param error An unknown type.
|
||||
*/
|
||||
static parseError(error: unknown): AppError {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static parseError(error: any): AppError {
|
||||
if (error instanceof AppError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Handle TRPC errors.
|
||||
if (error instanceof TRPCClientError) {
|
||||
if (error?.name === 'TRPCClientError') {
|
||||
const parsedJsonError = AppError.parseFromJSON(error.data?.appError);
|
||||
|
||||
const fallbackError = new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: error.message,
|
||||
message: error?.message,
|
||||
});
|
||||
|
||||
return parsedJsonError || fallbackError;
|
||||
|
||||
@ -6,6 +6,7 @@ import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-sig
|
||||
import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-deleted-email';
|
||||
import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email';
|
||||
import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email';
|
||||
import { BULK_SEND_TEMPLATE_JOB_DEFINITION } from './definitions/internal/bulk-send-template';
|
||||
import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
|
||||
|
||||
/**
|
||||
@ -21,6 +22,7 @@ export const jobsClient = new JobClient([
|
||||
SEAL_DOCUMENT_JOB_DEFINITION,
|
||||
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
||||
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
||||
BULK_SEND_TEMPLATE_JOB_DEFINITION,
|
||||
] as const);
|
||||
|
||||
export const jobs = jobsClient;
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID = 'send.bulk.complete.email';
|
||||
|
||||
const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
templateId: z.number(),
|
||||
templateName: z.string(),
|
||||
totalProcessed: z.number(),
|
||||
successCount: z.number(),
|
||||
failedCount: z.number(),
|
||||
errors: z.array(z.string()),
|
||||
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||
});
|
||||
|
||||
export type TSendBulkCompleteEmailJobDefinition = z.infer<
|
||||
typeof SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Bulk Complete Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-bulk-complete-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendBulkCompleteEmailJobDefinition
|
||||
>;
|
||||
@ -36,19 +36,19 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
User: true,
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
@ -62,7 +62,7 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -74,9 +74,9 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.Recipient;
|
||||
const [recipient] = document.recipients;
|
||||
const { email: recipientEmail, name: recipientName } = recipient;
|
||||
const { User: owner } = document;
|
||||
const { user: owner } = document;
|
||||
|
||||
const recipientReference = recipientName || recipientEmail;
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ export const run = async ({
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
@ -53,7 +53,7 @@ export const run = async ({
|
||||
}),
|
||||
]);
|
||||
|
||||
const { documentMeta, team, User: documentOwner } = document;
|
||||
const { documentMeta, user: documentOwner } = document;
|
||||
|
||||
const isEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
@ -70,7 +70,7 @@ export const run = async ({
|
||||
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
||||
recipientName: recipient.name,
|
||||
documentName: document.title,
|
||||
documentOwnerName: document.User.name || document.User.email,
|
||||
documentOwnerName: document.user.name || document.user.email,
|
||||
reason: recipient.rejectionReason || '',
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
@ -0,0 +1,208 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { parse } from 'csv-parse/sync';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { BulkSendCompleteEmail } from '@documenso/email/templates/bulk-send-complete';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { AppError } from '../../../errors/app-error';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TBulkSendTemplateJobDefinition } from './bulk-send-template';
|
||||
|
||||
const ZRecipientRowSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.union([
|
||||
z.string().email({ message: 'Value must be a valid email or empty string' }),
|
||||
z.string().max(0, { message: 'Value must be a valid email or empty string' }),
|
||||
]),
|
||||
});
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TBulkSendTemplateJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { userId, teamId, templateId, csvContent, sendImmediately, requestMetadata } = payload;
|
||||
|
||||
const template = await getTemplateById({
|
||||
id: templateId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
const rows = parse(csvContent, { columns: true, skip_empty_lines: true });
|
||||
|
||||
if (rows.length > 100) {
|
||||
throw new Error('Maximum 100 rows allowed per upload');
|
||||
}
|
||||
|
||||
const { recipients } = template;
|
||||
|
||||
// Validate CSV structure
|
||||
const csvHeaders = Object.keys(rows[0]);
|
||||
const requiredHeaders = recipients.map((_, index) => `recipient_${index + 1}_email`);
|
||||
|
||||
for (const header of requiredHeaders) {
|
||||
if (!csvHeaders.includes(header)) {
|
||||
throw new Error(`Missing required column: ${header}`);
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const results = {
|
||||
success: 0,
|
||||
failed: 0,
|
||||
errors: Array<string>(),
|
||||
};
|
||||
|
||||
// Process each row
|
||||
for (const [rowIndex, row] of rows.entries()) {
|
||||
try {
|
||||
for (const [recipientIndex] of recipients.entries()) {
|
||||
const nameKey = `recipient_${recipientIndex + 1}_name`;
|
||||
const emailKey = `recipient_${recipientIndex + 1}_email`;
|
||||
|
||||
const parsed = ZRecipientRowSchema.safeParse({
|
||||
name: row[nameKey],
|
||||
email: row[emailKey],
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error(
|
||||
`Invalid recipient data provided for ${emailKey}, ${nameKey}: ${parsed.error.issues?.[0]?.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const document = await io.runTask(`create-document-${rowIndex}`, async () => {
|
||||
return await createDocumentFromTemplate({
|
||||
templateId: template.id,
|
||||
userId,
|
||||
teamId,
|
||||
recipients: recipients.map((recipient, index) => {
|
||||
return {
|
||||
id: recipient.id,
|
||||
email: row[`recipient_${index + 1}_email`] || recipient.email,
|
||||
name: row[`recipient_${index + 1}_name`] || recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
};
|
||||
}),
|
||||
requestMetadata: {
|
||||
source: 'app',
|
||||
auth: 'session',
|
||||
requestMetadata: requestMetadata || {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (sendImmediately) {
|
||||
await io.runTask(`send-document-${rowIndex}`, async () => {
|
||||
await sendDocument({
|
||||
documentId: document.id,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata: {
|
||||
source: 'app',
|
||||
auth: 'session',
|
||||
requestMetadata: requestMetadata || {},
|
||||
},
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
throw new AppError('DOCUMENT_SEND_FAILED');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
results.success += 1;
|
||||
} catch (error) {
|
||||
results.failed += 1;
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
results.errors.push(`Row ${rowIndex + 1}: Was unable to be processed - ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
await io.runTask('send-completion-email', async () => {
|
||||
const completionTemplate = createElement(BulkSendCompleteEmail, {
|
||||
userName: user.name || user.email,
|
||||
templateName: template.title,
|
||||
totalProcessed: rows.length,
|
||||
successCount: results.success,
|
||||
failedCount: results.failed,
|
||||
errors: results.errors,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
let teamGlobalSettings: TeamGlobalSettings | undefined | null;
|
||||
|
||||
if (template.teamId) {
|
||||
teamGlobalSettings = await prisma.teamGlobalSettings.findUnique({
|
||||
where: {
|
||||
teamId: template.teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const branding = teamGlobalSettings
|
||||
? teamGlobalSettingsToBranding(teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const i18n = await getI18nInstance(teamGlobalSettings?.documentLanguage);
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(completionTemplate, {
|
||||
lang: teamGlobalSettings?.documentLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(completionTemplate, {
|
||||
lang: teamGlobalSettings?.documentLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: user.name || '',
|
||||
address: user.email,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`Bulk Send Complete: ${template.title}`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
};
|
||||
37
packages/lib/jobs/definitions/internal/bulk-send-template.ts
Normal file
37
packages/lib/jobs/definitions/internal/bulk-send-template.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const BULK_SEND_TEMPLATE_JOB_DEFINITION_ID = 'internal.bulk-send-template';
|
||||
|
||||
const BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
teamId: z.number().optional(),
|
||||
templateId: z.number(),
|
||||
csvContent: z.string(),
|
||||
sendImmediately: z.boolean(),
|
||||
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||
});
|
||||
|
||||
export type TBulkSendTemplateJobDefinition = z.infer<
|
||||
typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const BULK_SEND_TEMPLATE_JOB_DEFINITION = {
|
||||
id: BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
|
||||
name: 'Bulk Send Template',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
|
||||
schema: BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./bulk-send-template.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
|
||||
TBulkSendTemplateJobDefinition
|
||||
>;
|
||||
@ -20,7 +20,10 @@ import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
|
||||
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
|
||||
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import { ZWebhookDocumentSchema } from '../../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../../types/webhook-payload';
|
||||
import { getFile } from '../../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../../universal/upload/put-file';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
|
||||
@ -40,7 +43,7 @@ export const run = async ({
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
every: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
@ -48,7 +51,7 @@ export const run = async ({
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
teamGlobalSettings: {
|
||||
@ -102,7 +105,7 @@ export const run = async ({
|
||||
documentId: document.id,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -258,13 +261,13 @@ export const run = async ({
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/// <reference types="../types/next-auth.d.ts" />
|
||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||
import { compare } from '@node-rs/bcrypt';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { AuthOptions, Session, User } from 'next-auth';
|
||||
@ -27,8 +28,38 @@ import { extractNextAuthRequestMetadata } from '../universal/extract-request-met
|
||||
import { getAuthenticatorOptions } from '../utils/authenticator';
|
||||
import { ErrorCode } from './error-codes';
|
||||
|
||||
// Delete unrecognized fields from authorization response to comply with
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
|
||||
const prismaAdapter = PrismaAdapter(prisma);
|
||||
|
||||
const unsafe_linkAccount = prismaAdapter.linkAccount!;
|
||||
const unsafe_accountModel = Prisma.dmmf.datamodel.models.find(({ name }) => name === 'Account');
|
||||
|
||||
if (!unsafe_accountModel) {
|
||||
throw new Error('Account model not found');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
prismaAdapter.linkAccount = (data) => {
|
||||
const availableFields = unsafe_accountModel.fields.map((field) => field.name);
|
||||
|
||||
const newData = Object.keys(data).reduce(
|
||||
(acc, key) => {
|
||||
if (availableFields.includes(key)) {
|
||||
acc[key] = data[key];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
{} as typeof data,
|
||||
);
|
||||
|
||||
return unsafe_linkAccount(newData);
|
||||
};
|
||||
|
||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
adapter: PrismaAdapter(prisma),
|
||||
adapter: prismaAdapter,
|
||||
secret: process.env.NEXTAUTH_SECRET ?? 'secret',
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
@ -217,7 +248,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
|
||||
},
|
||||
include: {
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
@ -232,7 +263,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
throw new AppError(AppErrorCode.NOT_SETUP);
|
||||
}
|
||||
|
||||
const user = passkey.User;
|
||||
const user = passkey.user;
|
||||
|
||||
const { rpId, origin } = getAuthenticatorOptions();
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"@trigger.dev/sdk": "^2.3.18",
|
||||
"@upstash/redis": "^1.20.6",
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"inngest": "^3.19.13",
|
||||
"kysely": "^0.26.3",
|
||||
"luxon": "^3.4.0",
|
||||
|
||||
@ -33,7 +33,7 @@ export const setupTwoFactorAuthentication = async ({
|
||||
|
||||
const accountName = user.email;
|
||||
const uri = createTOTPKeyURI(ISSUER, accountName, secret);
|
||||
const encodedSecret = base32.encode(secret);
|
||||
const encodedSecret = base32.encode(new Uint8Array(secret));
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
|
||||
@ -30,14 +30,14 @@ export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocum
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
|
||||
@ -12,18 +12,18 @@ export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
|
||||
include: {
|
||||
documentMeta: true,
|
||||
documentAccessToken: true,
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
include: {
|
||||
Field: {
|
||||
fields: {
|
||||
include: {
|
||||
Signature: true,
|
||||
signature: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, Prisma } from '@documenso/prisma/client';
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type SigningVolume = {
|
||||
id: number;
|
||||
@ -24,92 +24,78 @@ export async function getSigningVolume({
|
||||
sortBy = 'signingVolume',
|
||||
sortOrder = 'desc',
|
||||
}: GetSigningVolumeOptions) {
|
||||
const whereClause = Prisma.validator<Prisma.SubscriptionWhereInput>()({
|
||||
status: 'ACTIVE',
|
||||
OR: [
|
||||
{
|
||||
User: {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
team: {
|
||||
name: { contains: search, mode: 'insensitive' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const offset = Math.max(page - 1, 0) * perPage;
|
||||
|
||||
const [subscriptions, totalCount] = await Promise.all([
|
||||
prisma.subscription.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
Document: {
|
||||
where: {
|
||||
status: DocumentStatus.COMPLETED,
|
||||
deletedAt: null,
|
||||
teamId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
document: {
|
||||
where: {
|
||||
status: DocumentStatus.COMPLETED,
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy:
|
||||
sortBy === 'name'
|
||||
? [{ User: { name: sortOrder } }, { team: { name: sortOrder } }, { createdAt: 'desc' }]
|
||||
: sortBy === 'createdAt'
|
||||
? [{ createdAt: sortOrder }]
|
||||
: undefined,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
}),
|
||||
prisma.subscription.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
let findQuery = kyselyPrisma.$kysely
|
||||
.selectFrom('Subscription as s')
|
||||
.leftJoin('User as u', 's.userId', 'u.id')
|
||||
.leftJoin('Team as t', 's.teamId', 't.id')
|
||||
.leftJoin('Document as ud', (join) =>
|
||||
join
|
||||
.onRef('u.id', '=', 'ud.userId')
|
||||
.on('ud.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('ud.deletedAt', 'is', null)
|
||||
.on('ud.teamId', 'is', null),
|
||||
)
|
||||
.leftJoin('Document as td', (join) =>
|
||||
join
|
||||
.onRef('t.id', '=', 'td.teamId')
|
||||
.on('td.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('td.deletedAt', 'is', null),
|
||||
)
|
||||
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('u.name', 'ilike', `%${search}%`),
|
||||
eb('u.email', 'ilike', `%${search}%`),
|
||||
eb('t.name', 'ilike', `%${search}%`),
|
||||
]),
|
||||
)
|
||||
.select([
|
||||
's.id as id',
|
||||
's.createdAt as createdAt',
|
||||
's.planId as planId',
|
||||
sql<string>`COALESCE(u.name, t.name, u.email, 'Unknown')`.as('name'),
|
||||
sql<number>`COUNT(DISTINCT ud.id) + COUNT(DISTINCT td.id)`.as('signingVolume'),
|
||||
])
|
||||
.groupBy(['s.id', 'u.name', 't.name', 'u.email']);
|
||||
|
||||
const leaderboardWithVolume: SigningVolume[] = subscriptions.map((subscription) => {
|
||||
const name =
|
||||
subscription.User?.name || subscription.team?.name || subscription.User?.email || 'Unknown';
|
||||
const userSignedDocs = subscription.User?.Document?.length || 0;
|
||||
const teamSignedDocs = subscription.team?.document?.length || 0;
|
||||
return {
|
||||
id: subscription.id,
|
||||
name,
|
||||
signingVolume: userSignedDocs + teamSignedDocs,
|
||||
createdAt: subscription.createdAt,
|
||||
planId: subscription.planId,
|
||||
};
|
||||
});
|
||||
|
||||
if (sortBy === 'signingVolume') {
|
||||
leaderboardWithVolume.sort((a, b) => {
|
||||
return sortOrder === 'desc'
|
||||
? b.signingVolume - a.signingVolume
|
||||
: a.signingVolume - b.signingVolume;
|
||||
});
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
findQuery = findQuery.orderBy('name', sortOrder);
|
||||
break;
|
||||
case 'createdAt':
|
||||
findQuery = findQuery.orderBy('createdAt', sortOrder);
|
||||
break;
|
||||
case 'signingVolume':
|
||||
findQuery = findQuery.orderBy('signingVolume', sortOrder);
|
||||
break;
|
||||
default:
|
||||
findQuery = findQuery.orderBy('signingVolume', 'desc');
|
||||
}
|
||||
|
||||
findQuery = findQuery.limit(perPage).offset(offset);
|
||||
|
||||
const countQuery = kyselyPrisma.$kysely
|
||||
.selectFrom('Subscription as s')
|
||||
.leftJoin('User as u', 's.userId', 'u.id')
|
||||
.leftJoin('Team as t', 's.teamId', 't.id')
|
||||
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('u.name', 'ilike', `%${search}%`),
|
||||
eb('u.email', 'ilike', `%${search}%`),
|
||||
eb('t.name', 'ilike', `%${search}%`),
|
||||
]),
|
||||
)
|
||||
.select(({ fn }) => [fn.countAll().as('count')]);
|
||||
|
||||
const [results, [{ count }]] = await Promise.all([findQuery.execute(), countQuery.execute()]);
|
||||
|
||||
return {
|
||||
leaderboard: leaderboardWithVolume,
|
||||
totalPages: Math.ceil(totalCount / perPage),
|
||||
leaderboard: results,
|
||||
totalPages: Math.ceil(Number(count) / perPage),
|
||||
};
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ export const getUsersCount = async () => {
|
||||
export const getUsersWithSubscriptionsCount = async () => {
|
||||
return await prisma.user.count({
|
||||
where: {
|
||||
Subscription: {
|
||||
subscriptions: {
|
||||
some: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
@ -22,7 +22,7 @@ export const getUsersWithSubscriptionsCount = async () => {
|
||||
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
|
||||
return await prisma.user.count({
|
||||
where: {
|
||||
Document: {
|
||||
documents: {
|
||||
some: {
|
||||
createdAt: {
|
||||
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
|
||||
@ -36,7 +36,7 @@ export const getUserWithAtLeastOneDocumentPerMonth = async () => {
|
||||
export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => {
|
||||
return await prisma.user.count({
|
||||
where: {
|
||||
Document: {
|
||||
documents: {
|
||||
some: {
|
||||
status: {
|
||||
equals: DocumentStatus.COMPLETED,
|
||||
|
||||
@ -23,7 +23,7 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
VerificationToken: {
|
||||
verificationTokens: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
@ -32,7 +32,7 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
|
||||
},
|
||||
});
|
||||
|
||||
const [verificationToken] = user.VerificationToken;
|
||||
const [verificationToken] = user.verificationTokens;
|
||||
|
||||
if (!verificationToken?.token) {
|
||||
throw new Error('Verification token not found for the user');
|
||||
|
||||
@ -20,7 +20,7 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
PasswordResetToken: {
|
||||
passwordResetTokens: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
@ -33,7 +33,7 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const token = user.PasswordResetToken[0].token;
|
||||
const token = user.passwordResetTokens[0].token;
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const resetPasswordLink = `${NEXT_PUBLIC_WEBAPP_URL()}/reset-password/${token}`;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffDocumentMetaChanges,
|
||||
@ -10,9 +10,12 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
@ -25,18 +28,18 @@ export type CreateDocumentMetaOptions = {
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
language?: SupportedLanguageCodes;
|
||||
userId: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const upsertDocumentMeta = async ({
|
||||
userId,
|
||||
teamId,
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
dateFormat,
|
||||
documentId,
|
||||
password,
|
||||
userId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
emailSettings,
|
||||
@ -45,40 +48,38 @@ export const upsertDocumentMeta = async ({
|
||||
language,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = document;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||
where: {
|
||||
@ -120,8 +121,7 @@ export const upsertDocumentMeta = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||
},
|
||||
|
||||
@ -14,7 +14,10 @@ import {
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendPendingEmail } from './send-pending-email';
|
||||
@ -31,7 +34,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
@ -39,7 +42,7 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
@ -59,11 +62,11 @@ export const completeDocumentWithToken = async ({
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error(`Document ${document.id} has no recipient with token ${token}`);
|
||||
}
|
||||
|
||||
const [recipient] = document.Recipient;
|
||||
const [recipient] = document.recipients;
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
@ -195,7 +198,7 @@ export const completeDocumentWithToken = async ({
|
||||
const haveAllRecipientsSigned = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
every: {
|
||||
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
|
||||
},
|
||||
@ -219,13 +222,13 @@ export const completeDocumentWithToken = async ({
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
});
|
||||
|
||||
248
packages/lib/server-only/document/create-document-v2.ts
Normal file
248
packages/lib/server-only/document/create-document-v2.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility, TemplateMeta } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
normalizePdf?: boolean;
|
||||
data: {
|
||||
title: string;
|
||||
externalId?: string;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
||||
globalActionAuth?: TDocumentActionAuthTypes;
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const createDocumentV2 = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentDataId,
|
||||
normalizePdf,
|
||||
data,
|
||||
meta,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues } = data;
|
||||
|
||||
const team = teamId
|
||||
? await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
if (teamId !== undefined && !team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (documentData) {
|
||||
const buffer = await getFile(documentData);
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
const newDocumentData = await putPdfFile({
|
||||
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
documentDataId = newDocumentData.id;
|
||||
}
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: data?.globalAccessAuth || null,
|
||||
globalActionAuth: data?.globalActionAuth || null,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const visibility = determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
team?.members[0].role ?? TeamMemberRole.MEMBER,
|
||||
);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
title,
|
||||
externalId: data.externalId,
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
...meta,
|
||||
signingOrder: meta?.signingOrder || undefined,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
});
|
||||
|
||||
await tx.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions: recipientAuthOptions,
|
||||
fields: {
|
||||
createMany: {
|
||||
data: (recipient.fields || []).map((field) => ({
|
||||
documentId: document.id,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Todo: Is it necessary to create a full audit log with all fields and recipients audit logs?
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
type: DocumentSource.DOCUMENT,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const createdDocument = await tx.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return createdDocument;
|
||||
});
|
||||
};
|
||||
@ -1,21 +1,22 @@
|
||||
'use server';
|
||||
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
@ -27,13 +28,9 @@ export type CreateDocumentOptions = {
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
normalizePdf?: boolean;
|
||||
timezone?: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const ZCreateDocumentResponseSchema = DocumentSchema;
|
||||
|
||||
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;
|
||||
|
||||
export const createDocument = async ({
|
||||
userId,
|
||||
title,
|
||||
@ -44,7 +41,7 @@ export const createDocument = async ({
|
||||
formValues,
|
||||
requestMetadata,
|
||||
timezone,
|
||||
}: CreateDocumentOptions): Promise<TCreateDocumentResponse> => {
|
||||
}: CreateDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
@ -92,25 +89,6 @@ export const createDocument = async ({
|
||||
userTeamRole = teamWithUserRole.members[0]?.role;
|
||||
}
|
||||
|
||||
const determineVisibility = (
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
if (globalVisibility) {
|
||||
return globalVisibility;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return DocumentVisibility.ADMIN;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
};
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
@ -142,7 +120,7 @@ export const createDocument = async ({
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
visibility: determineVisibility(
|
||||
visibility: determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
@ -162,8 +140,7 @@ export const createDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
@ -179,7 +156,7 @@ export const createDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -189,7 +166,7 @@ export const createDocument = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(createdDocument),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
@ -15,23 +15,29 @@ import type {
|
||||
TeamGlobalSettings,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteDocument = async ({
|
||||
@ -47,7 +53,9 @@ export const deleteDocument = async ({
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
@ -55,7 +63,7 @@ export const deleteDocument = async ({
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
@ -67,15 +75,19 @@ export const deleteDocument = async ({
|
||||
});
|
||||
|
||||
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserOwner = document.userId === userId;
|
||||
const isUserTeamMember = document.team?.members.some((member) => member.userId === userId);
|
||||
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
|
||||
const userRecipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||
throw new Error('Not allowed');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Not allowed',
|
||||
});
|
||||
}
|
||||
|
||||
// Handle hard or soft deleting the actual document if user has permission.
|
||||
@ -105,6 +117,13 @@ export const deleteDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CANCELLED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Return partial document for API v1 response.
|
||||
return {
|
||||
id: document.id,
|
||||
@ -121,7 +140,7 @@ export const deleteDocument = async ({
|
||||
|
||||
type HandleDocumentOwnerDeleteOptions = {
|
||||
document: Document & {
|
||||
Recipient: Recipient[];
|
||||
recipients: Recipient[];
|
||||
documentMeta: DocumentMeta | null;
|
||||
};
|
||||
team?:
|
||||
@ -130,7 +149,7 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
})
|
||||
| null;
|
||||
user: User;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
@ -150,8 +169,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
type: 'SOFT',
|
||||
},
|
||||
@ -177,8 +195,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
type: 'HARD',
|
||||
},
|
||||
@ -205,7 +222,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
|
||||
// Send cancellation emails to recipients.
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, type Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export interface DuplicateDocumentOptions {
|
||||
@ -11,24 +10,18 @@ export interface DuplicateDocumentOptions {
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export const ZDuplicateDocumentResponseSchema = z.object({
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export type TDuplicateDocumentResponse = z.infer<typeof ZDuplicateDocumentResponseSchema>;
|
||||
|
||||
export const duplicateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateDocumentOptions): Promise<TDuplicateDocumentResponse> => {
|
||||
}: DuplicateDocumentOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUniqueOrThrow({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
select: {
|
||||
title: true,
|
||||
@ -53,10 +46,16 @@ export const duplicateDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const createDocumentArguments: Prisma.DocumentCreateArgs = {
|
||||
data: {
|
||||
title: document.title,
|
||||
User: {
|
||||
user: {
|
||||
connect: {
|
||||
id: document.userId,
|
||||
},
|
||||
|
||||
@ -8,6 +8,7 @@ import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
@ -21,6 +22,7 @@ export interface FindDocumentAuditLogsOptions {
|
||||
|
||||
export const findDocumentAuditLogs = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
page = 1,
|
||||
perPage = 30,
|
||||
@ -34,20 +36,21 @@ export const findDocumentAuditLogs = async ({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type {
|
||||
@ -12,16 +11,10 @@ import type {
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSchema,
|
||||
RecipientSchema,
|
||||
TeamSchema,
|
||||
UserSchema,
|
||||
} from '@documenso/prisma/generated/zod';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
|
||||
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
|
||||
@ -43,23 +36,6 @@ export type FindDocumentsOptions = {
|
||||
query?: string;
|
||||
};
|
||||
|
||||
export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
|
||||
data: DocumentSchema.extend({
|
||||
User: UserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}),
|
||||
Recipient: RecipientSchema.array(),
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
url: true,
|
||||
}).nullable(),
|
||||
}).array(), // Todo: openapi remap.
|
||||
});
|
||||
|
||||
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;
|
||||
|
||||
export const findDocuments = async ({
|
||||
userId,
|
||||
teamId,
|
||||
@ -72,7 +48,7 @@ export const findDocuments = async ({
|
||||
period,
|
||||
senderIds,
|
||||
query,
|
||||
}: FindDocumentsOptions): Promise<TFindDocumentsResponse> => {
|
||||
}: FindDocumentsOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
@ -112,8 +88,8 @@ export const findDocuments = async ({
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -137,7 +113,7 @@ export const findDocuments = async ({
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -174,7 +150,7 @@ export const findDocuments = async ({
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
documentDeletedAt: null,
|
||||
@ -195,13 +171,13 @@ export const findDocuments = async ({
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
documentDeletedAt: null,
|
||||
@ -266,14 +242,14 @@ export const findDocuments = async ({
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -313,7 +289,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -321,7 +297,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -333,7 +309,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
status: {
|
||||
not: ExtendedDocumentStatus.DRAFT,
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -357,7 +333,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -378,7 +354,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -443,7 +419,7 @@ const findTeamDocumentsFilter = (
|
||||
status: {
|
||||
not: ExtendedDocumentStatus.DRAFT,
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
},
|
||||
@ -453,7 +429,7 @@ const findTeamDocumentsFilter = (
|
||||
|
||||
// Filter to display all documents that have been sent by the team email.
|
||||
filter.OR.push({
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
@ -472,7 +448,7 @@ const findTeamDocumentsFilter = (
|
||||
status: {
|
||||
not: ExtendedDocumentStatus.DRAFT,
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -498,7 +474,7 @@ const findTeamDocumentsFilter = (
|
||||
if (teamEmail && filter.OR) {
|
||||
filter.OR.push({
|
||||
status: ExtendedDocumentStatus.DRAFT,
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
@ -523,7 +499,7 @@ const findTeamDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -535,7 +511,7 @@ const findTeamDocumentsFilter = (
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
@ -560,7 +536,7 @@ const findTeamDocumentsFilter = (
|
||||
if (teamEmail && filter.OR) {
|
||||
filter.OR.push(
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
},
|
||||
@ -568,7 +544,7 @@ const findTeamDocumentsFilter = (
|
||||
OR: visibilityFilters,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
OR: visibilityFilters,
|
||||
|
||||
@ -26,14 +26,14 @@ export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumen
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
User: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
@ -119,14 +119,14 @@ export const getDocumentWhereInput = async ({
|
||||
if (team.teamEmail) {
|
||||
documentWhereInput.OR.push(
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
@ -154,7 +154,7 @@ export const getDocumentWhereInput = async ({
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
|
||||
@ -41,7 +41,7 @@ export const getDocumentByToken = async ({ token }: GetDocumentByTokenOptions) =
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
@ -66,17 +66,17 @@ export const getDocumentAndSenderByToken = async ({
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
user: true,
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
@ -96,9 +96,9 @@ export const getDocumentAndSenderByToken = async ({
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const { password: _password, ...User } = result.User;
|
||||
const { password: _password, ...user } = result.user;
|
||||
|
||||
const recipient = result.Recipient[0];
|
||||
const recipient = result.recipients[0];
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
@ -125,7 +125,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
|
||||
return {
|
||||
...result,
|
||||
User,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
@ -144,14 +144,14 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
@ -160,7 +160,7 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const [recipient] = result.Recipient;
|
||||
const [recipient] = result.recipients;
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
@ -185,8 +185,5 @@ export const getDocumentAndRecipientByToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
Recipient: result.Recipient,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -1,13 +1,4 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentDataSchema,
|
||||
DocumentMetaSchema,
|
||||
DocumentSchema,
|
||||
FieldSchema,
|
||||
RecipientSchema,
|
||||
} from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
@ -18,22 +9,11 @@ export type GetDocumentWithDetailsByIdOptions = {
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const ZGetDocumentWithDetailsByIdResponseSchema = DocumentSchema.extend({
|
||||
documentData: DocumentDataSchema,
|
||||
documentMeta: DocumentMetaSchema.nullable(),
|
||||
Recipient: RecipientSchema.array(),
|
||||
Field: FieldSchema.array(),
|
||||
});
|
||||
|
||||
export type TGetDocumentWithDetailsByIdResponse = z.infer<
|
||||
typeof ZGetDocumentWithDetailsByIdResponseSchema
|
||||
>;
|
||||
|
||||
export const getDocumentWithDetailsById = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWithDetailsByIdOptions): Promise<TGetDocumentWithDetailsByIdResponse> => {
|
||||
}: GetDocumentWithDetailsByIdOptions) => {
|
||||
const documentWhereInput = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
@ -45,8 +25,8 @@ export const getDocumentWithDetailsById = async ({
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
Field: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -85,8 +85,8 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -113,7 +113,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
},
|
||||
where: {
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -132,7 +132,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
},
|
||||
where: {
|
||||
createdAt,
|
||||
User: {
|
||||
user: {
|
||||
email: {
|
||||
not: user.email,
|
||||
},
|
||||
@ -140,7 +140,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
OR: [
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -150,7 +150,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -191,8 +191,8 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: options.search, mode: 'insensitive' } },
|
||||
{ Recipient: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ Recipient: { some: { email: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: options.search, mode: 'insensitive' } } } },
|
||||
],
|
||||
};
|
||||
|
||||
@ -234,7 +234,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
{
|
||||
OR: [
|
||||
{ userId: options.userId },
|
||||
{ Recipient: { some: { email: options.currentUserEmail } } },
|
||||
{ recipients: { some: { email: options.currentUserEmail } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -257,7 +257,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
User: {
|
||||
user: {
|
||||
email: teamEmail,
|
||||
},
|
||||
},
|
||||
@ -274,7 +274,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
@ -296,7 +296,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
OR: [
|
||||
{
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
@ -307,7 +307,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
{
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: teamEmail,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
@ -12,24 +9,16 @@ export type MoveDocumentToTeamOptions = {
|
||||
documentId: number;
|
||||
teamId: number;
|
||||
userId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
|
||||
|
||||
export type TMoveDocumentToTeamResponse = z.infer<typeof ZMoveDocumentToTeamResponseSchema>;
|
||||
|
||||
export const moveDocumentToTeam = async ({
|
||||
documentId,
|
||||
teamId,
|
||||
userId,
|
||||
requestMetadata,
|
||||
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
|
||||
}: MoveDocumentToTeamOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findUniqueOrThrow({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
const document = await tx.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -39,8 +28,7 @@ export const moveDocumentToTeam = async ({
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found or already associated with a team.',
|
||||
});
|
||||
}
|
||||
@ -57,9 +45,8 @@ export const moveDocumentToTeam = async ({
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You are not a member of this team.',
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'This team does not exist, or you are not a member of this team.',
|
||||
});
|
||||
}
|
||||
|
||||
@ -68,12 +55,11 @@ export const moveDocumentToTeam = async ({
|
||||
data: { teamId },
|
||||
});
|
||||
|
||||
const log = await tx.documentAuditLog.create({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||
documentId: updatedDocument.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
movedByUserId: userId,
|
||||
fromPersonalAccount: true,
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
@ -31,21 +34,20 @@ export async function rejectDocumentWithToken({
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
Document: {
|
||||
document: {
|
||||
include: {
|
||||
User: true,
|
||||
Recipient: true,
|
||||
user: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const document = recipient?.Document;
|
||||
const document = recipient?.document;
|
||||
|
||||
if (!recipient || !document) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document or recipient not found',
|
||||
});
|
||||
}
|
||||
@ -97,7 +99,7 @@ export async function rejectDocumentWithToken({
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
@ -109,7 +111,7 @@ export async function rejectDocumentWithToken({
|
||||
// Trigger webhook for document rejection
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_REJECTED,
|
||||
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '@documenso/lib/constants/recipient-roles';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -29,7 +29,7 @@ export type ResendDocumentOptions = {
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId?: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const resendDocument = async ({
|
||||
@ -54,7 +54,7 @@ export const resendDocument = async ({
|
||||
const document = await prisma.document.findUnique({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: {
|
||||
in: recipients,
|
||||
@ -80,7 +80,7 @@ export const resendDocument = async ({
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ export const resendDocument = async ({
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
@ -201,8 +201,7 @@ export const resendDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
|
||||
@ -14,7 +14,10 @@ import {
|
||||
} from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
@ -43,7 +46,7 @@ export const sealDocument = async ({
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
every: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
@ -52,7 +55,7 @@ export const sealDocument = async ({
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
teamGlobalSettings: {
|
||||
@ -89,7 +92,7 @@ export const sealDocument = async ({
|
||||
documentId: document.id,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -221,13 +224,13 @@ export const sealDocument = async ({
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
@ -35,7 +35,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: {
|
||||
contains: query,
|
||||
@ -48,7 +48,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
{
|
||||
status: DocumentStatus.COMPLETED,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -60,7 +60,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
{
|
||||
status: DocumentStatus.PENDING,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
@ -91,7 +91,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
url: true,
|
||||
@ -140,7 +140,7 @@ export const searchDocumentsWithKeyword = async ({
|
||||
return canAccessDocument;
|
||||
})
|
||||
.map((document) => {
|
||||
const { Recipient, ...documentWithoutRecipient } = document;
|
||||
const { recipients, ...documentWithoutRecipient } = document;
|
||||
|
||||
let documentPath;
|
||||
|
||||
@ -149,13 +149,13 @@ export const searchDocumentsWithKeyword = async ({
|
||||
} else if (document.teamId && document.team) {
|
||||
documentPath = `${formatDocumentsPath(document.team.url)}/${document.id}`;
|
||||
} else {
|
||||
documentPath = getSigningLink(Recipient, user);
|
||||
documentPath = getSigningLink(recipients, user);
|
||||
}
|
||||
|
||||
return {
|
||||
...documentWithoutRecipient,
|
||||
path: documentPath,
|
||||
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||
value: [document.id, document.title, ...document.recipients.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -32,8 +32,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
User: true,
|
||||
recipients: true,
|
||||
user: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -50,11 +50,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
|
||||
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { User: owner } = document;
|
||||
const { user: owner } = document;
|
||||
|
||||
const completedDocument = await getFile(document.documentData);
|
||||
|
||||
@ -83,7 +83,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
// - Recipient emails are disabled
|
||||
if (
|
||||
isOwnerDocumentCompletedEmailEnabled &&
|
||||
(!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
(!document.recipients.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled)
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
@ -150,7 +150,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
const customEmailTemplate = {
|
||||
'signer.name': recipient.name,
|
||||
'signer.email': recipient.email,
|
||||
|
||||
@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
@ -23,7 +24,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
user: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
include: {
|
||||
@ -34,7 +35,9 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
@ -45,7 +48,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, name } = document.User;
|
||||
const { email, name } = document.user;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -13,15 +11,13 @@ import {
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentMetaSchema,
|
||||
DocumentSchema,
|
||||
RecipientSchema,
|
||||
} from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
@ -31,34 +27,16 @@ export type SendDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
sendEmail?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
|
||||
documentMeta: DocumentMetaSchema.nullable(),
|
||||
Recipient: RecipientSchema.array(),
|
||||
});
|
||||
|
||||
export type TSendDocumentResponse = z.infer<typeof ZSendDocumentResponseSchema>;
|
||||
|
||||
export const sendDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
}: SendDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -79,7 +57,7 @@ export const sendDocument = async ({
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
},
|
||||
documentMeta: true,
|
||||
@ -91,7 +69,7 @@ export const sendDocument = async ({
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -101,13 +79,13 @@ export const sendDocument = async ({
|
||||
|
||||
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
let recipientsToNotify = document.Recipient;
|
||||
let recipientsToNotify = document.recipients;
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
// Get the currently active recipient.
|
||||
recipientsToNotify = document.Recipient.filter(
|
||||
(r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC,
|
||||
).slice(0, 1);
|
||||
recipientsToNotify = document.recipients
|
||||
.filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC)
|
||||
.slice(0, 1);
|
||||
|
||||
// Secondary filter so we aren't resending if the current active recipient has already
|
||||
// received the document.
|
||||
@ -198,14 +176,14 @@ export const sendDocument = async ({
|
||||
userId,
|
||||
documentId,
|
||||
recipientId: recipient.id,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const allRecipientsHaveNoActionToTake = document.Recipient.every(
|
||||
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
||||
(recipient) =>
|
||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
||||
);
|
||||
@ -215,7 +193,7 @@ export const sendDocument = async ({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
requestMetadata,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
@ -226,7 +204,7 @@ export const sendDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -237,8 +215,7 @@ export const sendDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||
documentId: document.id,
|
||||
requestMetadata,
|
||||
user,
|
||||
metadata: requestMetadata,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
@ -253,14 +230,14 @@ export const sendDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||
data: ZWebhookDocumentSchema.parse(updatedDocument),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
@ -21,14 +21,14 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
@ -46,7 +46,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length === 0) {
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.Recipient;
|
||||
const [recipient] = document.recipients;
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
@ -30,9 +31,9 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
User: true,
|
||||
user: true,
|
||||
team: {
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
@ -42,10 +43,12 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { status, User: user } = document;
|
||||
const { status, user } = document;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
@ -54,11 +57,11 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (
|
||||
status === DocumentStatus.PENDING &&
|
||||
document.Recipient.length > 0 &&
|
||||
document.recipients.length > 0 &&
|
||||
isDocumentDeletedEmailEnabled
|
||||
) {
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
document.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,281 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
|
||||
export type UpdateDocumentSettingsOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
data: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const ZUpdateDocumentSettingsResponseSchema = DocumentSchema;
|
||||
|
||||
export type TUpdateDocumentSettingsResponse = z.infer<typeof ZUpdateDocumentSettingsResponseSchema>;
|
||||
|
||||
export const updateDocumentSettings = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentSettingsOptions): Promise<TUpdateDocumentSettingsResponse> => {
|
||||
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Missing data to update',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data.visibility;
|
||||
|
||||
if (!isDocumentOwner) {
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the document has been sent',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTitleSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
to: newGlobalAccessAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalActionSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
to: newGlobalActionAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: auditLogs,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
@ -1,23 +1,40 @@
|
||||
'use server';
|
||||
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
documentId: number;
|
||||
data: Prisma.DocumentUpdateInput;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
data?: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const updateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentOptions) => {
|
||||
return await prisma.document.update({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
@ -36,8 +53,215 @@ export const updateDocument = async ({
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
const currentUserRole = document.team?.members[0]?.role;
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data?.visibility;
|
||||
|
||||
if (!isDocumentOwner) {
|
||||
match(currentUserRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If no data just return the document since this function is normally chained after a meta update.
|
||||
if (!data || Object.values(data).length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the document has been sent',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTitleSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
to: newGlobalAccessAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalActionSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
to: newGlobalActionAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: auditLogs,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
|
||||
@ -6,7 +6,10 @@ import { ReadStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
|
||||
import type { TDocumentAccessAuthTypes } from '../../types/document-auth';
|
||||
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type ViewedDocumentOptions = {
|
||||
@ -71,7 +74,7 @@ export const viewedDocument = async ({
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -81,7 +84,7 @@ export const viewedDocument = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_OPENED,
|
||||
data: ZWebhookDocumentSchema.parse(document),
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
|
||||
136
packages/lib/server-only/field/create-document-fields.ts
Normal file
136
packages/lib/server-only/field/create-document-fields.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface CreateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
fields: (TFieldAndMeta & {
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
})[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createDocumentFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: CreateDocumentFieldsOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
validatedFields.map(async (field) => {
|
||||
const createdField = await tx.field.create({
|
||||
data: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
documentId,
|
||||
recipientId: field.recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle field created audit log.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: createdField.secondaryId,
|
||||
fieldRecipientEmail: field.recipientEmail,
|
||||
fieldRecipientId: createdField.recipientId,
|
||||
fieldType: createdField.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return createdField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
@ -145,7 +145,7 @@ export const createField = async ({
|
||||
fieldMeta: result.data,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -160,7 +160,7 @@ export const createField = async ({
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId,
|
||||
fieldType: field.type,
|
||||
},
|
||||
|
||||
113
packages/lib/server-only/field/create-template-fields.ts
Normal file
113
packages/lib/server-only/field/create-template-fields.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface CreateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const createTemplateFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: CreateTemplateFieldsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'template not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the template.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
validatedFields.map(async (field) => {
|
||||
const createdField = await tx.field.create({
|
||||
data: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
templateId,
|
||||
recipientId: field.recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
return createdField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
122
packages/lib/server-only/field/delete-document-field.ts
Normal file
122
packages/lib/server-only/field/delete-document-field.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface DeleteDocumentFieldOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
fieldId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const deleteDocumentField = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentFieldOptions): Promise<void> => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!field) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
const documentId = field.documentId;
|
||||
|
||||
if (!documentId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field does not belong to a document. Use delete template field instead.',
|
||||
});
|
||||
}
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: field.recipientId,
|
||||
},
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient for field ${fieldId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, recipient.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Recipient has already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const deletedField = await tx.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle field deleted audit log.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: deletedField.secondaryId,
|
||||
fieldRecipientEmail: recipient.email,
|
||||
fieldRecipientId: deletedField.recipientId,
|
||||
fieldType: deletedField.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -22,7 +22,7 @@ export const deleteField = async ({
|
||||
const field = await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
Document: {
|
||||
document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
@ -42,7 +42,7 @@ export const deleteField = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -78,7 +78,7 @@ export const deleteField = async ({
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: field.recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
|
||||
48
packages/lib/server-only/field/delete-template-field.ts
Normal file
48
packages/lib/server-only/field/delete-template-field.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface DeleteTemplateFieldOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
fieldId: number;
|
||||
}
|
||||
|
||||
export const deleteTemplateField = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
}: DeleteTemplateFieldOptions): Promise<void> => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
template: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!field || !field.templateId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -11,14 +11,14 @@ export const getCompletedFieldsForDocument = async ({
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
Recipient: {
|
||||
recipient: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
inserted: true,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
Recipient: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
|
||||
@ -8,21 +8,21 @@ export type GetCompletedFieldsForTokenOptions = {
|
||||
export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
Document: {
|
||||
Recipient: {
|
||||
document: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
recipient: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
inserted: true,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
Recipient: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -13,23 +10,19 @@ export type GetFieldByIdOptions = {
|
||||
templateId?: number;
|
||||
};
|
||||
|
||||
export const ZGetFieldByIdResponseSchema = FieldSchema;
|
||||
|
||||
export type TGetFieldByIdResponse = z.infer<typeof ZGetFieldByIdResponseSchema>;
|
||||
|
||||
export const getFieldById = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
documentId,
|
||||
templateId,
|
||||
}: GetFieldByIdOptions): Promise<TGetFieldByIdResponse> => {
|
||||
}: GetFieldByIdOptions) => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
templateId,
|
||||
Document: {
|
||||
document: {
|
||||
OR:
|
||||
teamId === undefined
|
||||
? [
|
||||
|
||||
@ -3,34 +3,38 @@ import { prisma } from '@documenso/prisma';
|
||||
export interface GetFieldsForDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||
|
||||
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
||||
export const getFieldsForDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetFieldsForDocumentOptions) => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
Document: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
document: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
Recipient: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export interface GetFieldsForTemplateOptions {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForTemplateOptions) => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
Template: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
@ -7,12 +7,12 @@ export type GetFieldsForTokenOptions = {
|
||||
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipient: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -20,17 +20,17 @@ export const removeSignedFieldWithToken = async ({
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
Recipient: {
|
||||
recipient: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Document: true,
|
||||
Recipient: true,
|
||||
document: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { Document: document, Recipient: recipient } = field;
|
||||
const { document, recipient } = field;
|
||||
|
||||
if (!document) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
@ -16,7 +15,7 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffFieldChanges,
|
||||
@ -24,61 +23,46 @@ import {
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
fields: FieldData[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const ZSetFieldsForDocumentResponseSchema = z.object({
|
||||
fields: z.array(FieldSchema),
|
||||
});
|
||||
|
||||
export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocumentResponseSchema>;
|
||||
|
||||
export const setFieldsForDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: SetFieldsForDocumentOptions): Promise<TSetFieldsForDocumentResponse> => {
|
||||
}: SetFieldsForDocumentOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -99,7 +83,7 @@ export const setFieldsForDocument = async ({
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -110,7 +94,7 @@ export const setFieldsForDocument = async ({
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
const recipient = document.Recipient.find(
|
||||
const recipient = document.recipients.find(
|
||||
(recipient) => recipient.email.toLowerCase() === field.signerEmail.toLowerCase(),
|
||||
);
|
||||
|
||||
@ -245,12 +229,12 @@ export const setFieldsForDocument = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
Document: {
|
||||
document: {
|
||||
connect: {
|
||||
id: documentId,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
recipient: {
|
||||
connect: {
|
||||
documentId_email: {
|
||||
documentId,
|
||||
@ -280,8 +264,7 @@ export const setFieldsForDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
...baseAuditLog,
|
||||
@ -296,8 +279,7 @@ export const setFieldsForDocument = async ({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
},
|
||||
@ -325,11 +307,10 @@ export const setFieldsForDocument = async ({
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: field.recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
@ -16,10 +14,10 @@ import {
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
export type SetFieldsForTemplateOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
id?: number | null;
|
||||
@ -34,34 +32,30 @@ export type SetFieldsForTemplateOptions = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const ZSetFieldsForTemplateResponseSchema = z.object({
|
||||
fields: z.array(FieldSchema),
|
||||
});
|
||||
|
||||
export type TSetFieldsForTemplateResponse = z.infer<typeof ZSetFieldsForTemplateResponseSchema>;
|
||||
|
||||
export const setFieldsForTemplate = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
|
||||
}: SetFieldsForTemplateOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@ -74,7 +68,7 @@ export const setFieldsForTemplate = async ({
|
||||
templateId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -179,12 +173,12 @@ export const setFieldsForTemplate = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
Template: {
|
||||
template: {
|
||||
connect: {
|
||||
id: templateId,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
recipient: {
|
||||
connect: {
|
||||
templateId_email: {
|
||||
templateId,
|
||||
|
||||
@ -59,17 +59,17 @@ export const signFieldWithToken = async ({
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
Recipient: {
|
||||
recipient: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Document: true,
|
||||
Recipient: true,
|
||||
document: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { Document: document, Recipient: recipient } = field;
|
||||
const { document, recipient } = field;
|
||||
|
||||
if (!document) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
@ -213,7 +213,7 @@ export const signFieldWithToken = async ({
|
||||
|
||||
// Dirty but I don't want to deal with type information
|
||||
Object.assign(updatedField, {
|
||||
Signature: signature,
|
||||
signature,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
156
packages/lib/server-only/field/update-document-fields.ts
Normal file
156
packages/lib/server-only/field/update-document-fields.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffFieldChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface UpdateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
fields: {
|
||||
id: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const updateDocumentFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentFieldsOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = document.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Field with id ${field.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient attached to field ${field.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
originalField,
|
||||
updateData: field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
fieldsToUpdate.map(async ({ originalField, updateData, recipientEmail }) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
id: updateData.id,
|
||||
},
|
||||
data: {
|
||||
type: updateData.type,
|
||||
page: updateData.pageNumber,
|
||||
positionX: updateData.pageX,
|
||||
positionY: updateData.pageY,
|
||||
width: updateData.width,
|
||||
height: updateData.height,
|
||||
fieldMeta: updateData.fieldMeta,
|
||||
},
|
||||
});
|
||||
|
||||
const changes = diffFieldChanges(originalField, updatedField);
|
||||
|
||||
// Handle field updated audit log.
|
||||
if (changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
fieldRecipientEmail: recipientEmail,
|
||||
fieldRecipientId: updatedField.recipientId,
|
||||
fieldType: updatedField.type,
|
||||
changes,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return updatedField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: updatedFields,
|
||||
};
|
||||
};
|
||||
@ -44,7 +44,7 @@ export const updateField = async ({
|
||||
const oldField = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
Document: {
|
||||
document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
@ -65,11 +65,6 @@ export const updateField = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const newFieldMeta = {
|
||||
...(oldField.fieldMeta as FieldMeta),
|
||||
...fieldMeta,
|
||||
};
|
||||
|
||||
const field = prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
@ -83,10 +78,10 @@ export const updateField = async ({
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
fieldMeta: newFieldMeta,
|
||||
fieldMeta,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -127,7 +122,7 @@ export const updateField = async ({
|
||||
},
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
fieldRecipientEmail: updatedField.Recipient?.email ?? '',
|
||||
fieldRecipientEmail: updatedField.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId ?? -1,
|
||||
fieldType: updatedField.type,
|
||||
changes: diffFieldChanges(oldField, updatedField),
|
||||
|
||||
120
packages/lib/server-only/field/update-template-fields.ts
Normal file
120
packages/lib/server-only/field/update-template-fields.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
|
||||
export interface UpdateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
id: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const updateTemplateFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: UpdateTemplateFieldsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = template.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Field with id ${field.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = template.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient attached to field ${field.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
updateData: field,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
fieldsToUpdate.map(async ({ updateData }) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
id: updateData.id,
|
||||
},
|
||||
data: {
|
||||
type: updateData.type,
|
||||
page: updateData.pageNumber,
|
||||
positionX: updateData.pageX,
|
||||
positionY: updateData.pageY,
|
||||
width: updateData.width,
|
||||
height: updateData.height,
|
||||
fieldMeta: updateData.fieldMeta,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: updatedFields,
|
||||
};
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import type { PDFDocument } from 'pdf-lib';
|
||||
import { RotationTypes, degrees, radiansToDegrees } from 'pdf-lib';
|
||||
import { RotationTypes, degrees, radiansToDegrees, rgb } from 'pdf-lib';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
@ -36,6 +36,9 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
);
|
||||
|
||||
const isSignatureField = isSignatureFieldType(field.type);
|
||||
const isDebugMode =
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
|
||||
|
||||
pdf.registerFontkit(fontkit);
|
||||
|
||||
@ -83,6 +86,35 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
const fieldX = pageWidth * (Number(field.positionX) / 100);
|
||||
const fieldY = pageHeight * (Number(field.positionY) / 100);
|
||||
|
||||
// Draw debug box if debug mode is enabled
|
||||
if (isDebugMode) {
|
||||
let debugX = fieldX;
|
||||
let debugY = pageHeight - fieldY - fieldHeight; // Invert Y for PDF coordinates
|
||||
|
||||
if (pageRotationInDegrees !== 0) {
|
||||
const adjustedPosition = adjustPositionForRotation(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
debugX,
|
||||
debugY,
|
||||
pageRotationInDegrees,
|
||||
);
|
||||
|
||||
debugX = adjustedPosition.xPos;
|
||||
debugY = adjustedPosition.yPos;
|
||||
}
|
||||
|
||||
page.drawRectangle({
|
||||
x: debugX,
|
||||
y: debugY,
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
borderColor: rgb(1, 0, 0), // Red
|
||||
borderWidth: 1,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
}
|
||||
|
||||
const font = await pdf.embedFont(
|
||||
isSignatureField ? fontCaveat : fontNoto,
|
||||
isSignatureField ? { features: { calt: false } } : undefined,
|
||||
@ -98,8 +130,8 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
type: P.union(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE),
|
||||
},
|
||||
async (field) => {
|
||||
if (field.Signature?.signatureImageAsBase64) {
|
||||
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? '');
|
||||
if (field.signature?.signatureImageAsBase64) {
|
||||
const image = await pdf.embedPng(field.signature?.signatureImageAsBase64 ?? '');
|
||||
|
||||
let imageWidth = image.width;
|
||||
let imageHeight = image.height;
|
||||
@ -136,7 +168,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
} else {
|
||||
const signatureText = field.Signature?.typedSignature ?? '';
|
||||
const signatureText = field.signature?.typedSignature ?? '';
|
||||
|
||||
const longestLineInTextForWidth = signatureText
|
||||
.split('\n')
|
||||
@ -278,6 +310,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
const meta = Parser ? Parser.safeParse(field.fieldMeta) : null;
|
||||
|
||||
const customFontSize = meta?.success && meta.data.fontSize ? meta.data.fontSize : null;
|
||||
const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'center';
|
||||
const longestLineInTextForWidth = field.customText
|
||||
.split('\n')
|
||||
.sort((a, b) => b.length - a.length)[0];
|
||||
@ -293,7 +326,17 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
|
||||
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
||||
|
||||
let textX = fieldX + (fieldWidth - textWidth) / 2;
|
||||
// Add padding similar to web display (roughly 0.5rem equivalent in PDF units)
|
||||
const padding = 8; // PDF points, roughly equivalent to 0.5rem
|
||||
|
||||
// Calculate X position based on text alignment with padding
|
||||
let textX = fieldX + padding; // Left alignment starts after padding
|
||||
if (textAlign === 'center') {
|
||||
textX = fieldX + (fieldWidth - textWidth) / 2; // Center alignment ignores padding
|
||||
} else if (textAlign === 'right') {
|
||||
textX = fieldX + fieldWidth - textWidth - padding; // Right alignment respects right padding
|
||||
}
|
||||
|
||||
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
||||
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
|
||||
@ -61,7 +61,7 @@ export const getPublicProfileByUrl = async ({
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
Template: {
|
||||
templates: {
|
||||
where: {
|
||||
directLink: {
|
||||
enabled: true,
|
||||
@ -73,7 +73,7 @@ export const getPublicProfileByUrl = async ({
|
||||
},
|
||||
},
|
||||
// Subscriptions and teamMembers are used to calculate the badges.
|
||||
Subscription: {
|
||||
subscriptions: {
|
||||
where: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
@ -133,7 +133,7 @@ export const getPublicProfileByUrl = async ({
|
||||
if (IS_BILLING_ENABLED()) {
|
||||
const earlyAdopterPriceIds = await getCommunityPlanPriceIds();
|
||||
|
||||
const activeEarlyAdopterSub = user.Subscription.find(
|
||||
const activeEarlyAdopterSub = user.subscriptions.find(
|
||||
(subscription) =>
|
||||
subscription.status === SubscriptionStatus.ACTIVE &&
|
||||
earlyAdopterPriceIds.includes(subscription.priceId),
|
||||
@ -154,7 +154,7 @@ export const getPublicProfileByUrl = async ({
|
||||
url: profileUrl,
|
||||
avatarImageId: user.avatarImageId,
|
||||
name: user.name || '',
|
||||
templates: user.Template.filter(
|
||||
templates: user.templates.filter(
|
||||
(template): template is PublicDirectLinkTemplate =>
|
||||
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
|
||||
),
|
||||
|
||||
@ -3,13 +3,13 @@ import sharp from 'sharp';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
|
||||
export type SetAvatarImageOptions = {
|
||||
userId: number;
|
||||
teamId?: number | null;
|
||||
bytes?: string | null;
|
||||
requestMetadata?: RequestMetadata;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const setAvatarImage = async ({
|
||||
|
||||
@ -6,6 +6,7 @@ import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
// temporary choice for testing only
|
||||
import * as timeConstants from '../../constants/time';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { alphaid } from '../../universal/id';
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
@ -42,7 +43,9 @@ export const createApiToken = async ({
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new Error('You do not have permission to create a token for this team');
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create a token for this team',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,10 +59,6 @@ export const createApiToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!storedToken) {
|
||||
throw new Error('Failed to create the API token');
|
||||
}
|
||||
|
||||
return {
|
||||
id: storedToken.id,
|
||||
token: apiToken,
|
||||
|
||||
@ -7,14 +7,14 @@ export const getUserByApiToken = async ({ token }: { token: string }) => {
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
ApiToken: {
|
||||
apiTokens: {
|
||||
some: {
|
||||
token: hashedToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
ApiToken: true,
|
||||
apiTokens: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -22,7 +22,7 @@ export const getUserByApiToken = async ({ token }: { token: string }) => {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const retrievedToken = user.ApiToken.find((apiToken) => apiToken.token === hashedToken);
|
||||
const retrievedToken = user.apiTokens.find((apiToken) => apiToken.token === hashedToken);
|
||||
|
||||
// This should be impossible but we need to satisfy TypeScript
|
||||
if (!retrievedToken) {
|
||||
|
||||
156
packages/lib/server-only/recipient/create-document-recipients.ts
Normal file
156
packages/lib/server-only/recipient/create-document-recipients.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface CreateDocumentRecipientsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
recipients: {
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
}[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createDocumentRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
recipients: recipientsToCreate,
|
||||
requestMetadata,
|
||||
}: CreateDocumentRecipientsOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
||||
...recipient,
|
||||
email: recipient.email.toLowerCase(),
|
||||
}));
|
||||
|
||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
||||
const existingRecipient = document.recipients.find(
|
||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
||||
);
|
||||
|
||||
return existingRecipient !== undefined;
|
||||
});
|
||||
|
||||
if (duplicateRecipients.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
const authOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
});
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
documentId,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle recipient created audit log.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
documentId: documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: createdRecipient.email,
|
||||
recipientName: createdRecipient.name,
|
||||
recipientId: createdRecipient.id,
|
||||
recipientRole: createdRecipient.role,
|
||||
accessAuth: recipient.accessAuth || undefined,
|
||||
actionAuth: recipient.actionAuth || undefined,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return createdRecipient;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
recipients: createdRecipients,
|
||||
};
|
||||
};
|
||||
128
packages/lib/server-only/recipient/create-template-recipients.ts
Normal file
128
packages/lib/server-only/recipient/create-template-recipients.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface CreateTemplateRecipientsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
recipients: {
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
||||
actionAuth?: TRecipientActionAuthTypes | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const createTemplateRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
recipients: recipientsToCreate,
|
||||
}: CreateTemplateRecipientsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
||||
...recipient,
|
||||
email: recipient.email.toLowerCase(),
|
||||
}));
|
||||
|
||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
||||
const existingRecipient = template.recipients.find(
|
||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
||||
);
|
||||
|
||||
return existingRecipient !== undefined;
|
||||
});
|
||||
|
||||
if (duplicateRecipients.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
const authOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
});
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
templateId,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
return createdRecipient;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
recipients: createdRecipients,
|
||||
};
|
||||
};
|
||||
161
packages/lib/server-only/recipient/delete-document-recipient.ts
Normal file
161
packages/lib/server-only/recipient/delete-document-recipient.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
|
||||
export interface DeleteDocumentRecipientOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
recipientId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const deleteDocumentRecipient = async ({
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentRecipientOptions): Promise<void> => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
team: true,
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToDelete = document.recipients[0];
|
||||
|
||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
documentId: document.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipientToDelete.email,
|
||||
recipientName: recipientToDelete.name,
|
||||
recipientId: recipientToDelete.id,
|
||||
recipientRole: recipientToDelete.role,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).recipientRemoved;
|
||||
|
||||
// Send email to deleted recipient.
|
||||
if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) {
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: document.team?.name || user.name || undefined,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
|
||||
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipientToDelete.email,
|
||||
name: recipientToDelete.name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`You have been removed from a document`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -23,7 +23,7 @@ export const deleteRecipient = async ({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
Document: {
|
||||
document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface DeleteTemplateRecipientOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
recipientId: number;
|
||||
}
|
||||
|
||||
export const deleteTemplateRecipient = async ({
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
}: DeleteTemplateRecipientOptions): Promise<void> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToDelete = template.recipients[0];
|
||||
|
||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -8,7 +8,7 @@ export type GetIsRecipientTurnOptions = {
|
||||
export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOptions) {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
Recipient: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
@ -16,7 +16,7 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
Recipient: {
|
||||
recipients: {
|
||||
orderBy: {
|
||||
signingOrder: 'asc',
|
||||
},
|
||||
@ -28,7 +28,7 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt
|
||||
return true;
|
||||
}
|
||||
|
||||
const recipients = document.Recipient;
|
||||
const { recipients } = document;
|
||||
|
||||
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { FieldSchema, RecipientSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@ -11,12 +8,6 @@ export type GetRecipientByIdOptions = {
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const ZGetRecipientByIdResponseSchema = RecipientSchema.extend({
|
||||
Field: FieldSchema.array(),
|
||||
});
|
||||
|
||||
export type TGetRecipientByIdResponse = z.infer<typeof ZGetRecipientByIdResponseSchema>;
|
||||
|
||||
/**
|
||||
* Get a recipient by ID. This will also return the recipient signing token so
|
||||
* be careful when using this.
|
||||
@ -25,32 +16,28 @@ export const getRecipientById = async ({
|
||||
recipientId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetRecipientByIdOptions): Promise<TGetRecipientByIdResponse> => {
|
||||
}: GetRecipientByIdOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
Document: {
|
||||
OR: [
|
||||
teamId === undefined
|
||||
? {
|
||||
userId,
|
||||
teamId: null,
|
||||
}
|
||||
: {
|
||||
teamId,
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
document: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Field: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ export type GetRecipientSignaturesOptions = {
|
||||
export const getRecipientSignatures = async ({ recipientId }: GetRecipientSignaturesOptions) => {
|
||||
return await prisma.signature.findMany({
|
||||
where: {
|
||||
Field: {
|
||||
field: {
|
||||
recipientId,
|
||||
},
|
||||
},
|
||||
|
||||
@ -14,23 +14,21 @@ export const getRecipientsForDocument = async ({
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
Document: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
teamId,
|
||||
document: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
|
||||
@ -3,31 +3,32 @@ import { prisma } from '@documenso/prisma';
|
||||
export interface GetRecipientsForTemplateOptions {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export const getRecipientsForTemplate = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetRecipientsForTemplateOptions) => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
Template: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
template: teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user