mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: initial document audit logs implementation (#922)
Added initial implementation of document audit logs.
This commit is contained in:
@ -85,6 +85,7 @@ export const SinglePlayerClient = () => {
|
|||||||
setFields(
|
setFields(
|
||||||
data.fields.map((field, i) => ({
|
data.fields.map((field, i) => ({
|
||||||
id: i,
|
id: i,
|
||||||
|
secondaryId: i.toString(),
|
||||||
documentId: -1,
|
documentId: -1,
|
||||||
templateId: null,
|
templateId: null,
|
||||||
recipientId: -1,
|
recipientId: -1,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { headers } from 'next/headers';
|
||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@ -13,6 +14,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
|||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
|
import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
@ -39,13 +41,17 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestHeaders = Object.fromEntries(headers().entries());
|
||||||
|
|
||||||
|
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||||
|
|
||||||
const [document, fields, recipient] = await Promise.all([
|
const [document, fields, recipient] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
}).catch(() => null),
|
}).catch(() => null),
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
viewedDocument({ token }).catch(() => null),
|
viewedDocument({ token, requestMetadata }).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!document || !document.documentData || !recipient) {
|
if (!document || !document.documentData || !recipient) {
|
||||||
|
|||||||
@ -24,3 +24,9 @@ export const RECIPIENT_ROLES_DESCRIPTION: {
|
|||||||
roleName: 'Viewer',
|
roleName: 'Viewer',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
|
||||||
|
[RecipientRole.SIGNER]: 'SIGNING_REQUEST',
|
||||||
|
[RecipientRole.VIEWER]: 'VIEW_REQUEST',
|
||||||
|
[RecipientRole.APPROVER]: 'APPROVE_REQUEST',
|
||||||
|
} as const;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentDataType } from '@documenso/prisma/client';
|
import type { DocumentDataType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type CreateDocumentDataOptions = {
|
export type CreateDocumentDataOptions = {
|
||||||
type: DocumentDataType;
|
type: DocumentDataType;
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
'use server';
|
'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';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
export type CreateDocumentMetaOptions = {
|
export type CreateDocumentMetaOptions = {
|
||||||
@ -11,6 +17,7 @@ export type CreateDocumentMetaOptions = {
|
|||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertDocumentMeta = async ({
|
export const upsertDocumentMeta = async ({
|
||||||
@ -19,31 +26,47 @@ export const upsertDocumentMeta = async ({
|
|||||||
timezone,
|
timezone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
documentId,
|
documentId,
|
||||||
userId,
|
|
||||||
password,
|
password,
|
||||||
|
userId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
|
requestMetadata,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: 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: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
userId,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
team: {
|
team: {
|
||||||
members: {
|
members: {
|
||||||
some: {
|
some: {
|
||||||
userId,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await prisma.documentMeta.upsert({
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
@ -65,4 +88,19 @@ export const upsertDocumentMeta = async ({
|
|||||||
redirectUrl,
|
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';
|
'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 { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
@ -9,11 +12,13 @@ import { sendPendingEmail } from './send-pending-email';
|
|||||||
export type CompleteDocumentWithTokenOptions = {
|
export type CompleteDocumentWithTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocumentWithToken = async ({
|
export const completeDocumentWithToken = async ({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
|
requestMetadata,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
'use server';
|
'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({
|
const pendingRecipients = await prisma.recipient.count({
|
||||||
where: {
|
where: {
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
@ -99,6 +122,6 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (documents.count > 0) {
|
if (documents.count > 0) {
|
||||||
await sealDocument({ documentId: document.id });
|
await sealDocument({ documentId: document.id, requestMetadata });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
'use server';
|
'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';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
export type CreateDocumentOptions = {
|
export type CreateDocumentOptions = {
|
||||||
@ -7,6 +11,7 @@ export type CreateDocumentOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
documentDataId: string;
|
documentDataId: string;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDocument = async ({
|
export const createDocument = async ({
|
||||||
@ -14,22 +19,30 @@ export const createDocument = async ({
|
|||||||
title,
|
title,
|
||||||
documentDataId,
|
documentDataId,
|
||||||
teamId,
|
teamId,
|
||||||
|
requestMetadata,
|
||||||
}: CreateDocumentOptions) => {
|
}: CreateDocumentOptions) => {
|
||||||
return await prisma.$transaction(async (tx) => {
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
if (teamId !== undefined) {
|
|
||||||
await tx.team.findFirstOrThrow({
|
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
id: userId,
|
||||||
members: {
|
},
|
||||||
some: {
|
include: {
|
||||||
userId,
|
teamMembers: {
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
teamId !== undefined &&
|
||||||
|
!user.teamMembers.some((teamMember) => teamMember.teamId === teamId)
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, 'Team not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tx.document.create({
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const document = await tx.document.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
documentDataId,
|
documentDataId,
|
||||||
@ -37,5 +50,19 @@ export const createDocument = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||||
|
documentId: document.id,
|
||||||
|
user,
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return document;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,12 +4,18 @@ import { mailer } from '@documenso/email/mailer';
|
|||||||
import { render } from '@documenso/email/render';
|
import { render } from '@documenso/email/render';
|
||||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
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 { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import type { Prisma } 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 { getDocumentWhereInput } from './get-document-by-id';
|
||||||
|
|
||||||
export type ResendDocumentOptions = {
|
export type ResendDocumentOptions = {
|
||||||
@ -17,6 +23,7 @@ export type ResendDocumentOptions = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
recipients: number[];
|
recipients: number[];
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resendDocument = async ({
|
export const resendDocument = async ({
|
||||||
@ -24,6 +31,7 @@ export const resendDocument = async ({
|
|||||||
userId,
|
userId,
|
||||||
recipients,
|
recipients,
|
||||||
teamId,
|
teamId,
|
||||||
|
requestMetadata,
|
||||||
}: ResendDocumentOptions) => {
|
}: ResendDocumentOptions) => {
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@ -76,6 +84,8 @@ export const resendDocument = async ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
@ -99,6 +109,7 @@ export const resendDocument = async ({
|
|||||||
|
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
address: email,
|
address: email,
|
||||||
@ -114,6 +125,24 @@ export const resendDocument = async ({
|
|||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
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 { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
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 { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { signPdf } from '@documenso/signing';
|
import { signPdf } from '@documenso/signing';
|
||||||
|
|
||||||
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { putFile } from '../../universal/upload/put-file';
|
import { putFile } from '../../universal/upload/put-file';
|
||||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||||
@ -17,9 +20,14 @@ import { sendCompletedEmail } from './send-completed-email';
|
|||||||
export type SealDocumentOptions = {
|
export type SealDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
sendEmail?: boolean;
|
sendEmail?: boolean;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumentOptions) => {
|
export const sealDocument = async ({
|
||||||
|
documentId,
|
||||||
|
sendEmail = true,
|
||||||
|
requestMetadata,
|
||||||
|
}: SealDocumentOptions) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
@ -100,7 +108,8 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.documentData.update({
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.documentData.update({
|
||||||
where: {
|
where: {
|
||||||
id: documentData.id,
|
id: documentData.id,
|
||||||
},
|
},
|
||||||
@ -109,7 +118,20 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||||
|
documentId: document.id,
|
||||||
|
requestMetadata,
|
||||||
|
user: null,
|
||||||
|
data: {
|
||||||
|
transactionId: nanoid(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (sendEmail) {
|
if (sendEmail) {
|
||||||
await sendCompletedEmail({ documentId });
|
await sendCompletedEmail({ documentId, requestMetadata });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,13 +5,17 @@ import { render } from '@documenso/email/render';
|
|||||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
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 { getFile } from '../../universal/upload/get-file';
|
||||||
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
|
||||||
export interface SendDocumentOptions {
|
export interface SendDocumentOptions {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => {
|
export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDocumentOptions) => {
|
||||||
const document = await prisma.document.findUnique({
|
const document = await prisma.document.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
@ -44,6 +48,7 @@ export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) =>
|
|||||||
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
|
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
address: email,
|
address: email,
|
||||||
@ -63,6 +68,24 @@ export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) =>
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,22 +4,37 @@ import { mailer } from '@documenso/email/mailer';
|
|||||||
import { render } from '@documenso/email/render';
|
import { render } from '@documenso/email/render';
|
||||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
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 { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
|
||||||
|
|
||||||
export type SendDocumentOptions = {
|
export type SendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
userId: 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({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const document = await prisma.document.findUnique({
|
const document = await prisma.document.findUnique({
|
||||||
@ -66,6 +81,8 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
@ -89,6 +106,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
|
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
address: email,
|
address: email,
|
||||||
@ -105,7 +123,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.recipient.update({
|
await tx.recipient.update({
|
||||||
where: {
|
where: {
|
||||||
id: recipient.id,
|
id: recipient.id,
|
||||||
},
|
},
|
||||||
@ -113,6 +131,24 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
sendStatus: SendStatus.SENT,
|
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,15 +1,31 @@
|
|||||||
'use server';
|
'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 { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
export type UpdateTitleOptions = {
|
export type UpdateTitleOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTitle = async ({ userId, documentId, title }: UpdateTitleOptions) => {
|
export const updateTitle = async ({
|
||||||
return await prisma.document.update({
|
userId,
|
||||||
|
documentId,
|
||||||
|
title,
|
||||||
|
requestMetadata,
|
||||||
|
}: UpdateTitleOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.$transaction(async (tx) => {
|
||||||
|
const document = await tx.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
OR: [
|
OR: [
|
||||||
@ -27,8 +43,34 @@ export const updateTitle = async ({ userId, documentId, title }: UpdateTitleOpti
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.title === title) {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDocument = await tx.document.update({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
title,
|
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,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { prisma } from '@documenso/prisma';
|
||||||
import { ReadStatus } from '@documenso/prisma/client';
|
import { ReadStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type ViewedDocumentOptions = {
|
export type ViewedDocumentOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
export const viewedDocument = async ({ token, requestMetadata }: ViewedDocumentOptions) => {
|
||||||
const recipient = await prisma.recipient.findFirst({
|
const recipient = await prisma.recipient.findFirst({
|
||||||
where: {
|
where: {
|
||||||
token,
|
token,
|
||||||
@ -13,11 +17,14 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient || !recipient.documentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.recipient.update({
|
const { documentId } = recipient;
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.recipient.update({
|
||||||
where: {
|
where: {
|
||||||
id: recipient.id,
|
id: recipient.id,
|
||||||
},
|
},
|
||||||
@ -25,4 +32,23 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
|||||||
readStatus: ReadStatus.OPENED,
|
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';
|
'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 { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type RemovedSignedFieldWithTokenOptions = {
|
export type RemovedSignedFieldWithTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
fieldId: number;
|
fieldId: number;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeSignedFieldWithToken = async ({
|
export const removeSignedFieldWithToken = async ({
|
||||||
token,
|
token,
|
||||||
fieldId,
|
fieldId,
|
||||||
|
requestMetadata,
|
||||||
}: RemovedSignedFieldWithTokenOptions) => {
|
}: RemovedSignedFieldWithTokenOptions) => {
|
||||||
const field = await prisma.field.findFirstOrThrow({
|
const field = await prisma.field.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@ -44,8 +49,8 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
throw new Error(`Field ${fieldId} has no recipientId`);
|
throw new Error(`Field ${fieldId} has no recipientId`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await prisma.$transaction(async (tx) => {
|
||||||
prisma.field.update({
|
await tx.field.update({
|
||||||
where: {
|
where: {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
},
|
},
|
||||||
@ -53,11 +58,28 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
customText: '',
|
customText: '',
|
||||||
inserted: false,
|
inserted: false,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
prisma.signature.deleteMany({
|
|
||||||
|
await tx.signature.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
fieldId: field.id,
|
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 { prisma } from '@documenso/prisma';
|
||||||
import type { FieldType } from '@documenso/prisma/client';
|
import type { FieldType } from '@documenso/prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
@ -15,12 +21,14 @@ export interface SetFieldsForDocumentOptions {
|
|||||||
pageWidth: number;
|
pageWidth: number;
|
||||||
pageHeight: number;
|
pageHeight: number;
|
||||||
}[];
|
}[];
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setFieldsForDocument = async ({
|
export const setFieldsForDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
fields,
|
fields,
|
||||||
|
requestMetadata,
|
||||||
}: SetFieldsForDocumentOptions) => {
|
}: SetFieldsForDocumentOptions) => {
|
||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -42,6 +50,17 @@ export const setFieldsForDocument = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
@ -79,11 +98,12 @@ export const setFieldsForDocument = async ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedFields = await prisma.$transaction(
|
const persistedFields = await prisma.$transaction(async (tx) => {
|
||||||
// Disabling as wrapping promises here causes type issues
|
await Promise.all(
|
||||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
linkedFields.map(async (field) => {
|
||||||
linkedFields.map((field) =>
|
const fieldSignerEmail = field.signerEmail.toLowerCase();
|
||||||
prisma.field.upsert({
|
|
||||||
|
const upsertedField = await tx.field.upsert({
|
||||||
where: {
|
where: {
|
||||||
id: field._persisted?.id ?? -1,
|
id: field._persisted?.id ?? -1,
|
||||||
documentId,
|
documentId,
|
||||||
@ -113,23 +133,89 @@ export const setFieldsForDocument = async ({
|
|||||||
connect: {
|
connect: {
|
||||||
documentId_email: {
|
documentId_email: {
|
||||||
documentId,
|
documentId,
|
||||||
email: field.signerEmail.toLowerCase(),
|
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) {
|
if (removedFields.length > 0) {
|
||||||
await prisma.field.deleteMany({
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.field.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
in: removedFields.map((field) => field.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,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return persistedFields;
|
return persistedFields;
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
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 = {
|
export type SignFieldWithTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
fieldId: number;
|
fieldId: number;
|
||||||
value: string;
|
value: string;
|
||||||
isBase64?: boolean;
|
isBase64?: boolean;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signFieldWithToken = async ({
|
export const signFieldWithToken = async ({
|
||||||
@ -20,6 +25,7 @@ export const signFieldWithToken = async ({
|
|||||||
fieldId,
|
fieldId,
|
||||||
value,
|
value,
|
||||||
isBase64,
|
isBase64,
|
||||||
|
requestMetadata,
|
||||||
}: SignFieldWithTokenOptions) => {
|
}: SignFieldWithTokenOptions) => {
|
||||||
const field = await prisma.field.findFirstOrThrow({
|
const field = await prisma.field.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@ -40,6 +46,10 @@ export const signFieldWithToken = async ({
|
|||||||
throw new Error(`Document not found for field ${field.id}`);
|
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) {
|
if (document.status === DocumentStatus.COMPLETED) {
|
||||||
throw new Error(`Document ${document.id} has already been 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;
|
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 { prisma } from '@documenso/prisma';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { nanoid } from '../../universal/id';
|
|
||||||
|
|
||||||
export interface SetRecipientsForDocumentOptions {
|
export interface SetRecipientsForDocumentOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -13,12 +18,14 @@ export interface SetRecipientsForDocumentOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
}[];
|
}[];
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setRecipientsForDocument = async ({
|
export const setRecipientsForDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
recipients,
|
recipients,
|
||||||
|
requestMetadata,
|
||||||
}: SetRecipientsForDocumentOptions) => {
|
}: SetRecipientsForDocumentOptions) => {
|
||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -40,6 +47,17 @@ export const setRecipientsForDocument = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
@ -87,11 +105,10 @@ export const setRecipientsForDocument = async ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedRecipients = await prisma.$transaction(
|
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
||||||
// Disabling as wrapping promises here causes type issues
|
await Promise.all(
|
||||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
linkedRecipients.map(async (recipient) => {
|
||||||
linkedRecipients.map((recipient) =>
|
const upsertedRecipient = await tx.recipient.upsert({
|
||||||
prisma.recipient.upsert({
|
|
||||||
where: {
|
where: {
|
||||||
id: recipient._persisted?.id ?? -1,
|
id: recipient._persisted?.id ?? -1,
|
||||||
documentId,
|
documentId,
|
||||||
@ -115,18 +132,95 @@ export const setRecipientsForDocument = async ({
|
|||||||
signingStatus:
|
signingStatus:
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
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) {
|
if (removedRecipients.length > 0) {
|
||||||
await prisma.recipient.deleteMany({
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.recipient.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
in: removedRecipients.map((recipient) => recipient.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,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return persistedRecipients;
|
return persistedRecipients;
|
||||||
|
|||||||
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 = (
|
export const extractNextAuthRequestMetadata = (
|
||||||
req: Pick<RequestInternal, 'body' | 'query' | 'headers' | 'method'>,
|
req: Pick<RequestInternal, 'body' | 'query' | 'headers' | 'method'>,
|
||||||
): RequestMetadata => {
|
): 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 ipAddress = parsedIp.success ? parsedIp.data : undefined;
|
||||||
const userAgent = req.headers?.['user-agent'];
|
const userAgent = headers?.['user-agent'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ipAddress,
|
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;
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[secondaryId]` on the table `Field` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- The required column `secondaryId` was added to the `Field` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Field" ADD COLUMN "secondaryId" TEXT;
|
||||||
|
|
||||||
|
-- Set all null secondaryId fields to a uuid
|
||||||
|
UPDATE "Field" SET "secondaryId" = gen_random_uuid()::text WHERE "secondaryId" IS NULL;
|
||||||
|
|
||||||
|
-- Restrict the Field to required
|
||||||
|
ALTER TABLE "Field" ALTER COLUMN "secondaryId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DocumentAuditLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"documentId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"data" JSONB NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
"userId" INTEGER,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "DocumentAuditLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Field_secondaryId_key" ON "Field"("secondaryId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DocumentAuditLog" ADD CONSTRAINT "DocumentAuditLog_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -170,11 +170,30 @@ model Document {
|
|||||||
teamId Int?
|
teamId Int?
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
||||||
|
auditLogs DocumentAuditLog[]
|
||||||
|
|
||||||
@@unique([documentDataId])
|
@@unique([documentDataId])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DocumentAuditLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
documentId Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
type String
|
||||||
|
data Json
|
||||||
|
|
||||||
|
// Details of the person who performed the action which caused the audit log.
|
||||||
|
name String?
|
||||||
|
email String?
|
||||||
|
userId Int?
|
||||||
|
userAgent String?
|
||||||
|
ipAddress String?
|
||||||
|
|
||||||
|
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
enum DocumentDataType {
|
enum DocumentDataType {
|
||||||
S3_PATH
|
S3_PATH
|
||||||
BYTES
|
BYTES
|
||||||
@ -260,6 +279,7 @@ enum FieldType {
|
|||||||
|
|
||||||
model Field {
|
model Field {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
secondaryId String @unique @default(cuid())
|
||||||
documentId Int?
|
documentId Int?
|
||||||
templateId Int?
|
templateId Int?
|
||||||
recipientId Int?
|
recipientId Int?
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { updateTitle } from '@documenso/lib/server-only/document/update-title';
|
|||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||||
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
|
||||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -88,6 +89,7 @@ export const documentRouter = router({
|
|||||||
teamId,
|
teamId,
|
||||||
title,
|
title,
|
||||||
documentDataId,
|
documentDataId,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TRPCError) {
|
if (err instanceof TRPCError) {
|
||||||
@ -131,6 +133,7 @@ export const documentRouter = router({
|
|||||||
title,
|
title,
|
||||||
userId,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -144,6 +147,7 @@ export const documentRouter = router({
|
|||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
documentId,
|
documentId,
|
||||||
recipients,
|
recipients,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -166,6 +170,7 @@ export const documentRouter = router({
|
|||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
documentId,
|
documentId,
|
||||||
fields,
|
fields,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -198,6 +203,7 @@ export const documentRouter = router({
|
|||||||
documentId,
|
documentId,
|
||||||
password: securePassword,
|
password: securePassword,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -224,12 +230,14 @@ export const documentRouter = router({
|
|||||||
timezone: meta.timezone,
|
timezone: meta.timezone,
|
||||||
redirectUrl: meta.redirectUrl,
|
redirectUrl: meta.redirectUrl,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await sendDocument({
|
return await sendDocument({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
documentId,
|
documentId,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -248,6 +256,7 @@ export const documentRouter = router({
|
|||||||
return await resendDocument({
|
return await resendDocument({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
...input,
|
...input,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/rem
|
|||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||||
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
|
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
|
||||||
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
|
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
|
||||||
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
|
||||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -33,6 +34,7 @@ export const fieldRouter = router({
|
|||||||
pageWidth: field.pageWidth,
|
pageWidth: field.pageWidth,
|
||||||
pageHeight: field.pageHeight,
|
pageHeight: field.pageHeight,
|
||||||
})),
|
})),
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -67,7 +69,7 @@ export const fieldRouter = router({
|
|||||||
|
|
||||||
signFieldWithToken: procedure
|
signFieldWithToken: procedure
|
||||||
.input(ZSignFieldWithTokenMutationSchema)
|
.input(ZSignFieldWithTokenMutationSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { token, fieldId, value, isBase64 } = input;
|
const { token, fieldId, value, isBase64 } = input;
|
||||||
|
|
||||||
@ -76,6 +78,7 @@ export const fieldRouter = router({
|
|||||||
fieldId,
|
fieldId,
|
||||||
value,
|
value,
|
||||||
isBase64,
|
isBase64,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -89,13 +92,14 @@ export const fieldRouter = router({
|
|||||||
|
|
||||||
removeSignedFieldWithToken: procedure
|
removeSignedFieldWithToken: procedure
|
||||||
.input(ZRemovedSignedFieldWithTokenMutationSchema)
|
.input(ZRemovedSignedFieldWithTokenMutationSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { token, fieldId } = input;
|
const { token, fieldId } = input;
|
||||||
|
|
||||||
return await removeSignedFieldWithToken({
|
return await removeSignedFieldWithToken({
|
||||||
token,
|
token,
|
||||||
fieldId,
|
fieldId,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
|
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
|
||||||
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
|
||||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -27,6 +28,7 @@ export const recipientRouter = router({
|
|||||||
name: signer.name,
|
name: signer.name,
|
||||||
role: signer.role,
|
role: signer.role,
|
||||||
})),
|
})),
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -65,13 +67,14 @@ export const recipientRouter = router({
|
|||||||
|
|
||||||
completeDocumentWithToken: procedure
|
completeDocumentWithToken: procedure
|
||||||
.input(ZCompleteDocumentWithTokenMutationSchema)
|
.input(ZCompleteDocumentWithTokenMutationSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { token, documentId } = input;
|
const { token, documentId } = input;
|
||||||
|
|
||||||
return await completeDocumentWithToken({
|
return await completeDocumentWithToken({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export const singleplayerRouter = router({
|
|||||||
: null,
|
: null,
|
||||||
// Dummy data.
|
// Dummy data.
|
||||||
id: -1,
|
id: -1,
|
||||||
|
secondaryId: '-1',
|
||||||
documentId: -1,
|
documentId: -1,
|
||||||
templateId: null,
|
templateId: null,
|
||||||
recipientId: -1,
|
recipientId: -1,
|
||||||
|
|||||||
Reference in New Issue
Block a user