mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Merge branch 'main' into feat/add-runtime-env
This commit is contained in:
@ -24,3 +24,9 @@ export const RECIPIENT_ROLES_DESCRIPTION: {
|
||||
roleName: 'Viewer',
|
||||
},
|
||||
};
|
||||
|
||||
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
|
||||
[RecipientRole.SIGNER]: 'SIGNING_REQUEST',
|
||||
[RecipientRole.VIEWER]: 'VIEW_REQUEST',
|
||||
[RecipientRole.APPROVER]: 'APPROVE_REQUEST',
|
||||
} as const;
|
||||
|
||||
2
packages/lib/constants/url-regex.ts
Normal file
2
packages/lib/constants/url-regex.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const URL_REGEX =
|
||||
/^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;
|
||||
@ -1,7 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentDataType } from '@documenso/prisma/client';
|
||||
import type { DocumentDataType } from '@documenso/prisma/client';
|
||||
|
||||
export type CreateDocumentDataOptions = {
|
||||
type: DocumentDataType;
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
'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 {
|
||||
createDocumentAuditLogData,
|
||||
diffDocumentMetaChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
@ -9,7 +15,9 @@ export type CreateDocumentMetaOptions = {
|
||||
timezone?: string;
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
userId: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
};
|
||||
|
||||
export const upsertDocumentMeta = async ({
|
||||
@ -18,47 +26,81 @@ export const upsertDocumentMeta = async ({
|
||||
timezone,
|
||||
dateFormat,
|
||||
documentId,
|
||||
userId,
|
||||
password,
|
||||
userId,
|
||||
redirectUrl,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
await prisma.document.findFirstOrThrow({
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.documentMeta.upsert({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
create: {
|
||||
subject,
|
||||
message,
|
||||
dateFormat,
|
||||
timezone,
|
||||
password,
|
||||
documentId,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
message,
|
||||
dateFormat,
|
||||
password,
|
||||
timezone,
|
||||
},
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
create: {
|
||||
subject,
|
||||
message,
|
||||
password,
|
||||
dateFormat,
|
||||
timezone,
|
||||
documentId,
|
||||
redirectUrl,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
message,
|
||||
password,
|
||||
dateFormat,
|
||||
timezone,
|
||||
redirectUrl,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return upsertedDocumentMeta;
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
'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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
@ -9,11 +12,13 @@ import { sendPendingEmail } from './send-pending-email';
|
||||
export type CompleteDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
'use server';
|
||||
|
||||
@ -70,6 +75,24 @@ export const completeDocumentWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const pendingRecipients = await prisma.recipient.count({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
@ -99,6 +122,6 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
|
||||
if (documents.count > 0) {
|
||||
await sealDocument({ documentId: document.id });
|
||||
await sealDocument({ documentId: document.id, requestMetadata });
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
@ -7,6 +11,7 @@ export type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const createDocument = async ({
|
||||
@ -14,22 +19,30 @@ export const createDocument = async ({
|
||||
title,
|
||||
documentDataId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
if (teamId !== undefined) {
|
||||
await tx.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
teamMembers: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return await tx.document.create({
|
||||
if (
|
||||
teamId !== undefined &&
|
||||
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Team not found');
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
title,
|
||||
documentDataId,
|
||||
@ -37,5 +50,19 @@ export const createDocument = async ({
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return document;
|
||||
});
|
||||
};
|
||||
|
||||
@ -39,6 +39,7 @@ export const duplicateDocumentById = async ({
|
||||
dateFormat: true,
|
||||
password: true,
|
||||
timezone: true,
|
||||
redirectUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -27,6 +27,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
include: {
|
||||
User: true,
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -4,12 +4,18 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
@ -19,6 +25,7 @@ export type ResendDocumentOptions = {
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId?: number;
|
||||
requestMetadata: RequestMetadata;
|
||||
};
|
||||
|
||||
export const resendDocument = async ({
|
||||
@ -26,6 +33,7 @@ export const resendDocument = async ({
|
||||
userId,
|
||||
recipients,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: ResendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@ -78,6 +86,8 @@ export const resendDocument = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
const customEmailTemplate = {
|
||||
@ -101,20 +111,39 @@ export const resendDocument = async ({
|
||||
|
||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@ -5,10 +5,13 @@ import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putFile } from '../../universal/upload/put-file';
|
||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||
@ -17,9 +20,14 @@ import { sendCompletedEmail } from './send-completed-email';
|
||||
export type SealDocumentOptions = {
|
||||
documentId: number;
|
||||
sendEmail?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumentOptions) => {
|
||||
export const sealDocument = async ({
|
||||
documentId,
|
||||
sendEmail = true,
|
||||
requestMetadata,
|
||||
}: SealDocumentOptions) => {
|
||||
'use server';
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
@ -100,16 +108,30 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.documentData.update({
|
||||
where: {
|
||||
id: documentData.id,
|
||||
},
|
||||
data: {
|
||||
data: newData,
|
||||
},
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.documentData.update({
|
||||
where: {
|
||||
id: documentData.id,
|
||||
},
|
||||
data: {
|
||||
data: newData,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
requestMetadata,
|
||||
user: null,
|
||||
data: {
|
||||
transactionId: nanoid(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
if (sendEmail) {
|
||||
await sendCompletedEmail({ documentId });
|
||||
await sendCompletedEmail({ documentId, requestMetadata });
|
||||
}
|
||||
};
|
||||
|
||||
@ -6,13 +6,17 @@ import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/docum
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export interface SendDocumentOptions {
|
||||
documentId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => {
|
||||
export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
@ -45,24 +49,43 @@ export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) =>
|
||||
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
|
||||
});
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title,
|
||||
content: Buffer.from(buffer),
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||
},
|
||||
subject: 'Signing Complete!',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title,
|
||||
content: Buffer.from(buffer),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: 'DOCUMENT_COMPLETED',
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@ -4,6 +4,13 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { render } from '@documenso/email/render';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
@ -14,13 +21,23 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
|
||||
export const sendDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
@ -67,6 +84,8 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
const customEmailTemplate = {
|
||||
@ -90,29 +109,48 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
||||
|
||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
|
||||
await prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
await tx.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@ -1,34 +1,76 @@
|
||||
'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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type UpdateTitleOptions = {
|
||||
userId: number;
|
||||
documentId: number;
|
||||
title: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updateTitle = async ({ userId, documentId, title }: UpdateTitleOptions) => {
|
||||
return await prisma.document.update({
|
||||
export const updateTitle = async ({
|
||||
userId,
|
||||
documentId,
|
||||
title,
|
||||
requestMetadata,
|
||||
}: UpdateTitleOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (document.title === title) {
|
||||
return document;
|
||||
}
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: updatedDocument.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { ReadStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type ViewedDocumentOptions = {
|
||||
token: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
||||
export const viewedDocument = async ({ token, requestMetadata }: ViewedDocumentOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
@ -13,16 +17,38 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
if (!recipient || !recipient.documentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
readStatus: ReadStatus.OPENED,
|
||||
},
|
||||
const { documentId } = recipient;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
readStatus: ReadStatus.OPENED,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
documentId,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
'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 { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type RemovedSignedFieldWithTokenOptions = {
|
||||
token: string;
|
||||
fieldId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const removeSignedFieldWithToken = async ({
|
||||
token,
|
||||
fieldId,
|
||||
requestMetadata,
|
||||
}: RemovedSignedFieldWithTokenOptions) => {
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
@ -44,8 +49,8 @@ export const removeSignedFieldWithToken = async ({
|
||||
throw new Error(`Field ${fieldId} has no recipientId`);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
prisma.field.update({
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.field.update({
|
||||
where: {
|
||||
id: field.id,
|
||||
},
|
||||
@ -53,11 +58,28 @@ export const removeSignedFieldWithToken = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
},
|
||||
}),
|
||||
prisma.signature.deleteMany({
|
||||
});
|
||||
|
||||
await tx.signature.deleteMany({
|
||||
where: {
|
||||
fieldId: field.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
name: recipient?.name,
|
||||
email: recipient?.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
field: field.type,
|
||||
fieldId: field.secondaryId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } 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 { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
@ -15,12 +21,14 @@ export interface SetFieldsForDocumentOptions {
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
}[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
export const setFieldsForDocument = async ({
|
||||
userId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: SetFieldsForDocumentOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
@ -42,10 +50,25 @@ export const setFieldsForDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new Error('Document already complete');
|
||||
}
|
||||
|
||||
const existingFields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
@ -75,56 +98,123 @@ export const setFieldsForDocument = async ({
|
||||
);
|
||||
});
|
||||
|
||||
const persistedFields = await prisma.$transaction(
|
||||
// Disabling as wrapping promises here causes type issues
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
linkedFields.map((field) =>
|
||||
prisma.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
documentId,
|
||||
},
|
||||
update: {
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
},
|
||||
create: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
Document: {
|
||||
connect: {
|
||||
id: documentId,
|
||||
},
|
||||
const persistedFields = await prisma.$transaction(async (tx) => {
|
||||
await Promise.all(
|
||||
linkedFields.map(async (field) => {
|
||||
const fieldSignerEmail = field.signerEmail.toLowerCase();
|
||||
|
||||
const upsertedField = await tx.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
documentId,
|
||||
},
|
||||
Recipient: {
|
||||
connect: {
|
||||
documentId_email: {
|
||||
documentId,
|
||||
email: field.signerEmail.toLowerCase(),
|
||||
update: {
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
},
|
||||
create: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
Document: {
|
||||
connect: {
|
||||
id: documentId,
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
connect: {
|
||||
documentId_email: {
|
||||
documentId,
|
||||
email: fieldSignerEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (upsertedField.recipientId === null) {
|
||||
throw new Error('Not possible');
|
||||
}
|
||||
|
||||
const baseAuditLog = {
|
||||
fieldId: upsertedField.secondaryId,
|
||||
fieldRecipientEmail: fieldSignerEmail,
|
||||
fieldRecipientId: upsertedField.recipientId,
|
||||
fieldType: upsertedField.type,
|
||||
};
|
||||
|
||||
const changes = field._persisted ? diffFieldChanges(field._persisted, upsertedField) : [];
|
||||
|
||||
// Handle field updated audit log.
|
||||
if (field._persisted && changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
...baseAuditLog,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Handle field created audit log.
|
||||
if (!field._persisted) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return upsertedField;
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
if (removedFields.length > 0) {
|
||||
await prisma.field.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: removedFields.map((field) => field.id),
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.field.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: removedFields.map((field) => field.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: removedFields.map((field) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientId: field.recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
'use server';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type SignFieldWithTokenOptions = {
|
||||
token: string;
|
||||
fieldId: number;
|
||||
value: string;
|
||||
isBase64?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const signFieldWithToken = async ({
|
||||
@ -20,6 +25,7 @@ export const signFieldWithToken = async ({
|
||||
fieldId,
|
||||
value,
|
||||
isBase64,
|
||||
requestMetadata,
|
||||
}: SignFieldWithTokenOptions) => {
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
@ -40,6 +46,10 @@ export const signFieldWithToken = async ({
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error(`Recipient not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
throw new Error(`Document ${document.id} has already been completed`);
|
||||
}
|
||||
@ -123,6 +133,38 @@ export const signFieldWithToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
fieldId: updatedField.secondaryId,
|
||||
field: match(updatedField.type)
|
||||
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, (type) => ({
|
||||
type,
|
||||
data: signatureImageAsBase64 || typedSignature || '',
|
||||
}))
|
||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.TEXT, (type) => ({
|
||||
type,
|
||||
data: updatedField.customText,
|
||||
}))
|
||||
.exhaustive(),
|
||||
fieldSecurity: {
|
||||
type: 'NONE',
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return updatedField;
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffRecipientChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { nanoid } from '../../universal/id';
|
||||
|
||||
export interface SetRecipientsForDocumentOptions {
|
||||
userId: number;
|
||||
documentId: number;
|
||||
@ -13,12 +18,14 @@ export interface SetRecipientsForDocumentOptions {
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
}[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
export const setRecipientsForDocument = async ({
|
||||
userId,
|
||||
documentId,
|
||||
recipients,
|
||||
requestMetadata,
|
||||
}: SetRecipientsForDocumentOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
@ -40,10 +47,25 @@ export const setRecipientsForDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new Error('Document already complete');
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
email: recipient.email.toLowerCase(),
|
||||
@ -77,49 +99,127 @@ export const setRecipientsForDocument = async ({
|
||||
})
|
||||
.filter((recipient) => {
|
||||
return (
|
||||
recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
|
||||
recipient._persisted?.role === RecipientRole.CC ||
|
||||
(recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED)
|
||||
);
|
||||
});
|
||||
|
||||
const persistedRecipients = await prisma.$transaction(
|
||||
// Disabling as wrapping promises here causes type issues
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
linkedRecipients.map((recipient) =>
|
||||
prisma.recipient.upsert({
|
||||
where: {
|
||||
id: recipient._persisted?.id ?? -1,
|
||||
documentId,
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
documentId,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
create: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
token: nanoid(),
|
||||
documentId,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
||||
await Promise.all(
|
||||
linkedRecipients.map(async (recipient) => {
|
||||
const upsertedRecipient = await tx.recipient.upsert({
|
||||
where: {
|
||||
id: recipient._persisted?.id ?? -1,
|
||||
documentId,
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
documentId,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
create: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
token: nanoid(),
|
||||
documentId,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientId = upsertedRecipient.id;
|
||||
|
||||
// Clear all fields if the recipient role is changed to a type that cannot have fields.
|
||||
if (
|
||||
recipient._persisted &&
|
||||
recipient._persisted.role !== recipient.role &&
|
||||
(recipient.role === RecipientRole.CC || recipient.role === RecipientRole.VIEWER)
|
||||
) {
|
||||
await tx.field.deleteMany({
|
||||
where: {
|
||||
recipientId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const baseAuditLog = {
|
||||
recipientEmail: upsertedRecipient.email,
|
||||
recipientName: upsertedRecipient.name,
|
||||
recipientId,
|
||||
recipientRole: upsertedRecipient.role,
|
||||
};
|
||||
|
||||
const changes = recipient._persisted
|
||||
? diffRecipientChanges(recipient._persisted, upsertedRecipient)
|
||||
: [];
|
||||
|
||||
// Handle recipient updated audit log.
|
||||
if (recipient._persisted && changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
...baseAuditLog,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Handle recipient created audit log.
|
||||
if (!recipient._persisted) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: baseAuditLog,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return upsertedRecipient;
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
if (removedRecipients.length > 0) {
|
||||
await prisma.recipient.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: removedRecipients.map((recipient) => recipient.id),
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.recipient.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: removedRecipients.map((recipient) => recipient.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: removedRecipients.map((recipient) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
documentId: documentId,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
350
packages/lib/types/document-audit-logs.ts
Normal file
350
packages/lib/types/document-audit-logs.ts
Normal file
@ -0,0 +1,350 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Be aware that any changes to this file may require migrations since we are storing JSON
|
||||
// data in Prisma.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
// Document actions.
|
||||
'EMAIL_SENT',
|
||||
|
||||
// Document modification events.
|
||||
'FIELD_CREATED',
|
||||
'FIELD_DELETED',
|
||||
'FIELD_UPDATED',
|
||||
'RECIPIENT_CREATED',
|
||||
'RECIPIENT_DELETED',
|
||||
'RECIPIENT_UPDATED',
|
||||
|
||||
// Document events.
|
||||
'DOCUMENT_COMPLETED',
|
||||
'DOCUMENT_CREATED',
|
||||
'DOCUMENT_DELETED',
|
||||
'DOCUMENT_FIELD_INSERTED',
|
||||
'DOCUMENT_FIELD_UNINSERTED',
|
||||
'DOCUMENT_META_UPDATED',
|
||||
'DOCUMENT_OPENED',
|
||||
'DOCUMENT_TITLE_UPDATED',
|
||||
'DOCUMENT_RECIPIENT_COMPLETED',
|
||||
]);
|
||||
|
||||
export const ZDocumentMetaDiffTypeSchema = z.enum([
|
||||
'DATE_FORMAT',
|
||||
'MESSAGE',
|
||||
'PASSWORD',
|
||||
'REDIRECT_URL',
|
||||
'SUBJECT',
|
||||
'TIMEZONE',
|
||||
]);
|
||||
export const ZFieldDiffTypeSchema = z.enum(['DIMENSION', 'POSITION']);
|
||||
export const ZRecipientDiffTypeSchema = z.enum(['NAME', 'ROLE', 'EMAIL']);
|
||||
|
||||
export const DOCUMENT_AUDIT_LOG_TYPE = ZDocumentAuditLogTypeSchema.Enum;
|
||||
export const DOCUMENT_META_DIFF_TYPE = ZDocumentMetaDiffTypeSchema.Enum;
|
||||
export const FIELD_DIFF_TYPE = ZFieldDiffTypeSchema.Enum;
|
||||
export const RECIPIENT_DIFF_TYPE = ZRecipientDiffTypeSchema.Enum;
|
||||
|
||||
export const ZFieldDiffDimensionSchema = z.object({
|
||||
type: z.literal(FIELD_DIFF_TYPE.DIMENSION),
|
||||
from: z.object({
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
}),
|
||||
to: z.object({
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZFieldDiffPositionSchema = z.object({
|
||||
type: z.literal(FIELD_DIFF_TYPE.POSITION),
|
||||
from: z.object({
|
||||
page: z.number(),
|
||||
positionX: z.number(),
|
||||
positionY: z.number(),
|
||||
}),
|
||||
to: z.object({
|
||||
page: z.number(),
|
||||
positionX: z.number(),
|
||||
positionY: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogDocumentMetaSchema = z.union([
|
||||
z.object({
|
||||
type: z.union([
|
||||
z.literal(DOCUMENT_META_DIFF_TYPE.DATE_FORMAT),
|
||||
z.literal(DOCUMENT_META_DIFF_TYPE.MESSAGE),
|
||||
z.literal(DOCUMENT_META_DIFF_TYPE.REDIRECT_URL),
|
||||
z.literal(DOCUMENT_META_DIFF_TYPE.SUBJECT),
|
||||
z.literal(DOCUMENT_META_DIFF_TYPE.TIMEZONE),
|
||||
]),
|
||||
from: z.string().nullable(),
|
||||
to: z.string().nullable(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(DOCUMENT_META_DIFF_TYPE.PASSWORD),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const ZDocumentAuditLogFieldDiffSchema = z.union([
|
||||
ZFieldDiffDimensionSchema,
|
||||
ZFieldDiffPositionSchema,
|
||||
]);
|
||||
|
||||
export const ZRecipientDiffNameSchema = z.object({
|
||||
type: z.literal(RECIPIENT_DIFF_TYPE.NAME),
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
});
|
||||
|
||||
export const ZRecipientDiffRoleSchema = z.object({
|
||||
type: z.literal(RECIPIENT_DIFF_TYPE.ROLE),
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
});
|
||||
|
||||
export const ZRecipientDiffEmailSchema = z.object({
|
||||
type: z.literal(RECIPIENT_DIFF_TYPE.EMAIL),
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogRecipientDiffSchema = z.union([
|
||||
ZRecipientDiffNameSchema,
|
||||
ZRecipientDiffRoleSchema,
|
||||
ZRecipientDiffEmailSchema,
|
||||
]);
|
||||
|
||||
const ZBaseFieldEventDataSchema = z.object({
|
||||
fieldId: z.string(), // Note: This is the secondary field ID, which will get migrated in the future.
|
||||
fieldRecipientEmail: z.string(),
|
||||
fieldRecipientId: z.number(),
|
||||
fieldType: z.string(), // We specifically don't want to use enums to allow for more flexibility.
|
||||
});
|
||||
|
||||
const ZBaseRecipientDataSchema = z.object({
|
||||
recipientEmail: z.string(),
|
||||
recipientName: z.string(),
|
||||
recipientId: z.number(),
|
||||
recipientRole: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Email sent.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventEmailSentSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT),
|
||||
data: ZBaseRecipientDataSchema.extend({
|
||||
emailType: z.enum([
|
||||
'SIGNING_REQUEST',
|
||||
'VIEW_REQUEST',
|
||||
'APPROVE_REQUEST',
|
||||
'CC',
|
||||
'DOCUMENT_COMPLETED',
|
||||
]),
|
||||
isResending: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document completed.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentCompletedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED),
|
||||
data: z.object({
|
||||
transactionId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document created.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentCreatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED),
|
||||
data: z.object({
|
||||
title: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document field inserted.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED),
|
||||
data: ZBaseRecipientDataSchema.extend({
|
||||
fieldId: z.string(),
|
||||
|
||||
// Organised into union to allow us to extend each field if required.
|
||||
field: z.union([
|
||||
z.object({
|
||||
type: z.literal(FieldType.EMAIL),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.DATE),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NAME),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.TEXT),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.union([z.literal(FieldType.SIGNATURE), z.literal(FieldType.FREE_SIGNATURE)]),
|
||||
data: z.string(),
|
||||
}),
|
||||
]),
|
||||
|
||||
// Todo: Replace with union once we have more field security types.
|
||||
fieldSecurity: z.object({
|
||||
type: z.literal('NONE'),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document field uninserted.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentFieldUninsertedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED),
|
||||
data: z.object({
|
||||
field: z.nativeEnum(FieldType),
|
||||
fieldId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document meta updated.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentMetaUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED),
|
||||
data: z.object({
|
||||
changes: z.array(ZDocumentAuditLogDocumentMetaSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document opened.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED),
|
||||
data: ZBaseRecipientDataSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document recipient completed the document (the recipient has fully actioned and completed their required steps for the document).
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED),
|
||||
data: ZBaseRecipientDataSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document title updated.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentTitleUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED),
|
||||
data: z.object({
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Field created.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventFieldCreatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED),
|
||||
data: ZBaseFieldEventDataSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Field deleted.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventFieldRemovedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED),
|
||||
data: ZBaseFieldEventDataSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Field updated.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventFieldUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED),
|
||||
data: ZBaseFieldEventDataSchema.extend({
|
||||
changes: z.array(ZDocumentAuditLogFieldDiffSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Recipient added.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventRecipientAddedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
|
||||
data: ZBaseRecipientDataSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Recipient updated.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventRecipientUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED),
|
||||
data: ZBaseRecipientDataSchema.extend({
|
||||
changes: z.array(ZDocumentAuditLogRecipientDiffSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Recipient deleted.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventRecipientRemovedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED),
|
||||
data: ZBaseRecipientDataSchema,
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
z.union([
|
||||
ZDocumentAuditLogEventEmailSentSchema,
|
||||
ZDocumentAuditLogEventDocumentCompletedSchema,
|
||||
ZDocumentAuditLogEventDocumentCreatedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentOpenedSchema,
|
||||
ZDocumentAuditLogEventDocumentRecipientCompleteSchema,
|
||||
ZDocumentAuditLogEventDocumentTitleUpdatedSchema,
|
||||
ZDocumentAuditLogEventFieldCreatedSchema,
|
||||
ZDocumentAuditLogEventFieldRemovedSchema,
|
||||
ZDocumentAuditLogEventFieldUpdatedSchema,
|
||||
ZDocumentAuditLogEventRecipientAddedSchema,
|
||||
ZDocumentAuditLogEventRecipientUpdatedSchema,
|
||||
ZDocumentAuditLogEventRecipientRemovedSchema,
|
||||
]),
|
||||
);
|
||||
|
||||
export type TDocumentAuditLog = z.infer<typeof ZDocumentAuditLogSchema>;
|
||||
export type TDocumentAuditLogType = z.infer<typeof ZDocumentAuditLogTypeSchema>;
|
||||
|
||||
export type TDocumentAuditLogFieldDiffSchema = z.infer<typeof ZDocumentAuditLogFieldDiffSchema>;
|
||||
|
||||
export type TDocumentAuditLogDocumentMetaDiffSchema = z.infer<
|
||||
typeof ZDocumentAuditLogDocumentMetaSchema
|
||||
>;
|
||||
|
||||
export type TDocumentAuditLogRecipientDiffSchema = z.infer<
|
||||
typeof ZDocumentAuditLogRecipientDiffSchema
|
||||
>;
|
||||
@ -25,10 +25,16 @@ export const extractNextApiRequestMetadata = (req: NextApiRequest): RequestMetad
|
||||
export const extractNextAuthRequestMetadata = (
|
||||
req: Pick<RequestInternal, 'body' | 'query' | 'headers' | 'method'>,
|
||||
): RequestMetadata => {
|
||||
const parsedIp = ZIpSchema.safeParse(req.headers?.['x-forwarded-for']);
|
||||
return extractNextHeaderRequestMetadata(req.headers ?? {});
|
||||
};
|
||||
|
||||
export const extractNextHeaderRequestMetadata = (
|
||||
headers: Record<string, string>,
|
||||
): RequestMetadata => {
|
||||
const parsedIp = ZIpSchema.safeParse(headers?.['x-forwarded-for']);
|
||||
|
||||
const ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
||||
const userAgent = req.headers?.['user-agent'];
|
||||
const userAgent = headers?.['user-agent'];
|
||||
|
||||
return {
|
||||
ipAddress,
|
||||
|
||||
205
packages/lib/utils/document-audit-logs.ts
Normal file
205
packages/lib/utils/document-audit-logs.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@documenso/prisma/client';
|
||||
|
||||
import type {
|
||||
TDocumentAuditLog,
|
||||
TDocumentAuditLogDocumentMetaDiffSchema,
|
||||
TDocumentAuditLogFieldDiffSchema,
|
||||
TDocumentAuditLogRecipientDiffSchema,
|
||||
} from '../types/document-audit-logs';
|
||||
import {
|
||||
DOCUMENT_META_DIFF_TYPE,
|
||||
FIELD_DIFF_TYPE,
|
||||
RECIPIENT_DIFF_TYPE,
|
||||
ZDocumentAuditLogSchema,
|
||||
} from '../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../universal/extract-request-metadata';
|
||||
|
||||
type CreateDocumentAuditLogDataOptions<T = TDocumentAuditLog['type']> = {
|
||||
documentId: number;
|
||||
type: T;
|
||||
data: Extract<TDocumentAuditLog, { type: T }>['data'];
|
||||
user: { email?: string; id?: number | null; name?: string | null } | null;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
type CreateDocumentAuditLogDataResponse = Pick<
|
||||
DocumentAuditLog,
|
||||
'type' | 'ipAddress' | 'userAgent' | 'email' | 'userId' | 'name' | 'documentId'
|
||||
> & {
|
||||
data: TDocumentAuditLog['data'];
|
||||
};
|
||||
|
||||
export const createDocumentAuditLogData = ({
|
||||
documentId,
|
||||
type,
|
||||
data,
|
||||
user,
|
||||
requestMetadata,
|
||||
}: CreateDocumentAuditLogDataOptions): CreateDocumentAuditLogDataResponse => {
|
||||
return {
|
||||
type,
|
||||
data,
|
||||
documentId,
|
||||
userId: user?.id ?? null,
|
||||
email: user?.email ?? null,
|
||||
name: user?.name ?? null,
|
||||
userAgent: requestMetadata?.userAgent ?? null,
|
||||
ipAddress: requestMetadata?.ipAddress ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a raw document audit log from Prisma, to a typed audit log.
|
||||
*
|
||||
* @param auditLog raw audit log from Prisma.
|
||||
*/
|
||||
export const parseDocumentAuditLogData = (auditLog: DocumentAuditLog): TDocumentAuditLog => {
|
||||
const data = ZDocumentAuditLogSchema.safeParse(auditLog);
|
||||
|
||||
// Handle any required migrations here.
|
||||
if (!data.success) {
|
||||
throw new Error('Migration required');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
};
|
||||
|
||||
type PartialRecipient = Pick<Recipient, 'email' | 'name' | 'role'>;
|
||||
|
||||
export const diffRecipientChanges = (
|
||||
oldRecipient: PartialRecipient,
|
||||
newRecipient: PartialRecipient,
|
||||
): TDocumentAuditLogRecipientDiffSchema[] => {
|
||||
const diffs: TDocumentAuditLogRecipientDiffSchema[] = [];
|
||||
|
||||
if (oldRecipient.email !== newRecipient.email) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
||||
from: oldRecipient.email,
|
||||
to: newRecipient.email,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldRecipient.role !== newRecipient.role) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.ROLE,
|
||||
from: oldRecipient.role,
|
||||
to: newRecipient.role,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldRecipient.name !== newRecipient.name) {
|
||||
diffs.push({
|
||||
type: RECIPIENT_DIFF_TYPE.NAME,
|
||||
from: oldRecipient.name,
|
||||
to: newRecipient.name,
|
||||
});
|
||||
}
|
||||
|
||||
return diffs;
|
||||
};
|
||||
|
||||
export const diffFieldChanges = (
|
||||
oldField: Field,
|
||||
newField: Field,
|
||||
): TDocumentAuditLogFieldDiffSchema[] => {
|
||||
const diffs: TDocumentAuditLogFieldDiffSchema[] = [];
|
||||
|
||||
if (
|
||||
oldField.page !== newField.page ||
|
||||
!oldField.positionX.equals(newField.positionX) ||
|
||||
!oldField.positionY.equals(newField.positionY)
|
||||
) {
|
||||
diffs.push({
|
||||
type: FIELD_DIFF_TYPE.POSITION,
|
||||
from: {
|
||||
page: oldField.page,
|
||||
positionX: oldField.positionX.toNumber(),
|
||||
positionY: oldField.positionY.toNumber(),
|
||||
},
|
||||
to: {
|
||||
page: newField.page,
|
||||
positionX: newField.positionX.toNumber(),
|
||||
positionY: newField.positionY.toNumber(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!oldField.width.equals(newField.width) || !oldField.height.equals(newField.height)) {
|
||||
diffs.push({
|
||||
type: FIELD_DIFF_TYPE.DIMENSION,
|
||||
from: {
|
||||
width: oldField.width.toNumber(),
|
||||
height: oldField.height.toNumber(),
|
||||
},
|
||||
to: {
|
||||
width: newField.width.toNumber(),
|
||||
height: newField.height.toNumber(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return diffs;
|
||||
};
|
||||
|
||||
export const diffDocumentMetaChanges = (
|
||||
oldData: Partial<DocumentMeta> = {},
|
||||
newData: DocumentMeta,
|
||||
): TDocumentAuditLogDocumentMetaDiffSchema[] => {
|
||||
const diffs: TDocumentAuditLogDocumentMetaDiffSchema[] = [];
|
||||
|
||||
const oldDateFormat = oldData?.dateFormat ?? '';
|
||||
const oldMessage = oldData?.message ?? '';
|
||||
const oldSubject = oldData?.subject ?? '';
|
||||
const oldTimezone = oldData?.timezone ?? '';
|
||||
const oldPassword = oldData?.password ?? null;
|
||||
const oldRedirectUrl = oldData?.redirectUrl ?? '';
|
||||
|
||||
if (oldDateFormat !== newData.dateFormat) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.DATE_FORMAT,
|
||||
from: oldData?.dateFormat ?? '',
|
||||
to: newData.dateFormat,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldMessage !== newData.message) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.MESSAGE,
|
||||
from: oldMessage,
|
||||
to: newData.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldSubject !== newData.subject) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.SUBJECT,
|
||||
from: oldSubject,
|
||||
to: newData.subject,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldTimezone !== newData.timezone) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.TIMEZONE,
|
||||
from: oldTimezone,
|
||||
to: newData.timezone,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldRedirectUrl !== newData.redirectUrl) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.REDIRECT_URL,
|
||||
from: oldRedirectUrl,
|
||||
to: newData.redirectUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldPassword !== newData.password) {
|
||||
diffs.push({
|
||||
type: DOCUMENT_META_DIFF_TYPE.PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
return diffs;
|
||||
};
|
||||
Reference in New Issue
Block a user