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:
@ -86,6 +86,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,
|
||||||
|
|||||||
@ -151,7 +151,7 @@ export const EditDocumentForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||||
const { subject, message, timezone, dateFormat } = data.meta;
|
const { subject, message, timezone, dateFormat, redirectUrl } = data.meta;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendDocument({
|
await sendDocument({
|
||||||
@ -159,8 +159,9 @@ export const EditDocumentForm = ({
|
|||||||
meta: {
|
meta: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
timezone,
|
|
||||||
dateFormat,
|
dateFormat,
|
||||||
|
timezone,
|
||||||
|
redirectUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
{recipient?.role !== RecipientRole.CC && (
|
{recipient && recipient?.role !== RecipientRole.CC && (
|
||||||
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
{recipient?.role === RecipientRole.VIEWER && (
|
{recipient?.role === RecipientRole.VIEWER && (
|
||||||
|
|||||||
@ -26,9 +26,10 @@ export type SigningFormProps = {
|
|||||||
document: Document;
|
document: Document;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
|
redirectUrl?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -74,7 +75,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(`/sign/${recipient.token}/complete`);
|
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -118,7 +118,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
|
|||||||
<span className="text-muted-foreground">({recipient.email})</span>
|
<span className="text-muted-foreground">({recipient.email})</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<div className="py-4">
|
<div>
|
||||||
<Label htmlFor="signature">Full Name</Label>
|
<Label htmlFor="signature">Full Name</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -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';
|
||||||
@ -8,12 +9,12 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id';
|
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
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';
|
||||||
@ -40,24 +41,26 @@ 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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null);
|
|
||||||
|
|
||||||
if (!document || !document.documentData || !recipient) {
|
if (!document || !document.documentData || !recipient) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncatedTitle = truncateTitle(document.title);
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
|
|
||||||
const { documentData } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
const { user } = await getServerComponentSession();
|
const { user } = await getServerComponentSession();
|
||||||
|
|
||||||
@ -65,7 +68,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
document.status === DocumentStatus.COMPLETED ||
|
document.status === DocumentStatus.COMPLETED ||
|
||||||
recipient.signingStatus === SigningStatus.SIGNED
|
recipient.signingStatus === SigningStatus.SIGNED
|
||||||
) {
|
) {
|
||||||
redirect(`/sign/${token}/complete`);
|
documentMeta?.redirectUrl
|
||||||
|
? redirect(documentMeta.redirectUrl)
|
||||||
|
: redirect(`/sign/${token}/complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (documentMeta?.password) {
|
if (documentMeta?.password) {
|
||||||
@ -133,7 +138,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
||||||
<SigningForm document={document} recipient={recipient} fields={fields} />
|
<SigningForm
|
||||||
|
document={document}
|
||||||
|
recipient={recipient}
|
||||||
|
fields={fields}
|
||||||
|
redirectUrl={documentMeta?.redirectUrl}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -14519,6 +14519,7 @@
|
|||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
|
||||||
"integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
|
"integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@ -19511,14 +19512,14 @@
|
|||||||
"@react-email/section": "0.0.10",
|
"@react-email/section": "0.0.10",
|
||||||
"@react-email/tailwind": "0.0.9",
|
"@react-email/tailwind": "0.0.9",
|
||||||
"@react-email/text": "0.0.6",
|
"@react-email/text": "0.0.6",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.9",
|
||||||
"react-email": "^1.9.5",
|
"react-email": "^1.9.5",
|
||||||
"resend": "^2.0.0"
|
"resend": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"tsup": "^7.1.0"
|
"tsup": "^7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -19536,6 +19537,14 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/email/node_modules/nodemailer": {
|
||||||
|
"version": "6.9.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz",
|
||||||
|
"integrity": "sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/eslint-config": {
|
"packages/eslint-config": {
|
||||||
"name": "@documenso/eslint-config",
|
"name": "@documenso/eslint-config",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|||||||
@ -35,14 +35,14 @@
|
|||||||
"@react-email/section": "0.0.10",
|
"@react-email/section": "0.0.10",
|
||||||
"@react-email/tailwind": "0.0.9",
|
"@react-email/tailwind": "0.0.9",
|
||||||
"@react-email/text": "0.0.6",
|
"@react-email/text": "0.0.6",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.9",
|
||||||
"react-email": "^1.9.5",
|
"react-email": "^1.9.5",
|
||||||
"resend": "^2.0.0"
|
"resend": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"tsup": "^7.1.0"
|
"tsup": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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';
|
'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 = {
|
||||||
@ -9,7 +15,9 @@ export type CreateDocumentMetaOptions = {
|
|||||||
timezone?: string;
|
timezone?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
requestMetadata: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertDocumentMeta = async ({
|
export const upsertDocumentMeta = async ({
|
||||||
@ -18,47 +26,81 @@ export const upsertDocumentMeta = async ({
|
|||||||
timezone,
|
timezone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
documentId,
|
documentId,
|
||||||
userId,
|
|
||||||
password,
|
password,
|
||||||
|
userId,
|
||||||
|
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) => {
|
||||||
where: {
|
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||||
documentId,
|
where: {
|
||||||
},
|
documentId,
|
||||||
create: {
|
},
|
||||||
subject,
|
create: {
|
||||||
message,
|
subject,
|
||||||
dateFormat,
|
message,
|
||||||
timezone,
|
password,
|
||||||
password,
|
dateFormat,
|
||||||
documentId,
|
timezone,
|
||||||
},
|
documentId,
|
||||||
update: {
|
redirectUrl,
|
||||||
subject,
|
},
|
||||||
message,
|
update: {
|
||||||
dateFormat,
|
subject,
|
||||||
password,
|
message,
|
||||||
timezone,
|
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';
|
'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) {
|
where: {
|
||||||
await tx.team.findFirstOrThrow({
|
id: userId,
|
||||||
where: {
|
},
|
||||||
id: teamId,
|
include: {
|
||||||
members: {
|
teamMembers: {
|
||||||
some: {
|
select: {
|
||||||
userId,
|
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: {
|
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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export const duplicateDocumentById = async ({
|
|||||||
dateFormat: true,
|
dateFormat: true,
|
||||||
password: true,
|
password: true,
|
||||||
timezone: true,
|
timezone: true,
|
||||||
|
redirectUrl: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export const getDocumentAndSenderByToken = async ({
|
|||||||
include: {
|
include: {
|
||||||
User: true,
|
User: true,
|
||||||
documentData: true,
|
documentData: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
@ -19,6 +25,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 ({
|
||||||
@ -26,6 +33,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: {
|
||||||
@ -78,6 +86,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 = {
|
||||||
@ -101,20 +111,39 @@ export const resendDocument = async ({
|
|||||||
|
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
await mailer.sendMail({
|
await prisma.$transaction(async (tx) => {
|
||||||
to: {
|
await mailer.sendMail({
|
||||||
address: email,
|
to: {
|
||||||
name,
|
address: email,
|
||||||
},
|
name,
|
||||||
from: {
|
},
|
||||||
name: FROM_NAME,
|
from: {
|
||||||
address: FROM_ADDRESS,
|
name: FROM_NAME,
|
||||||
},
|
address: FROM_ADDRESS,
|
||||||
subject: customEmail?.subject
|
},
|
||||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
subject: customEmail?.subject
|
||||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
html: render(template),
|
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||||
text: render(template, { plainText: true }),
|
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 { 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,16 +108,30 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.documentData.update({
|
await prisma.$transaction(async (tx) => {
|
||||||
where: {
|
await tx.documentData.update({
|
||||||
id: documentData.id,
|
where: {
|
||||||
},
|
id: documentData.id,
|
||||||
data: {
|
},
|
||||||
data: newData,
|
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) {
|
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 { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
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 { 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,
|
||||||
@ -45,24 +49,43 @@ export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) =>
|
|||||||
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
|
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendMail({
|
await prisma.$transaction(async (tx) => {
|
||||||
to: {
|
await mailer.sendMail({
|
||||||
address: email,
|
to: {
|
||||||
name,
|
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),
|
|
||||||
},
|
},
|
||||||
],
|
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 { 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';
|
||||||
@ -14,13 +21,23 @@ 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({
|
||||||
@ -67,6 +84,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 = {
|
||||||
@ -90,29 +109,48 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
|
|
||||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||||
|
|
||||||
await mailer.sendMail({
|
await prisma.$transaction(async (tx) => {
|
||||||
to: {
|
await mailer.sendMail({
|
||||||
address: email,
|
to: {
|
||||||
name,
|
address: email,
|
||||||
},
|
name,
|
||||||
from: {
|
},
|
||||||
name: FROM_NAME,
|
from: {
|
||||||
address: FROM_ADDRESS,
|
name: FROM_NAME,
|
||||||
},
|
address: FROM_ADDRESS,
|
||||||
subject: customEmail?.subject
|
},
|
||||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
subject: customEmail?.subject
|
||||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||||
html: render(template),
|
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||||
text: render(template, { plainText: true }),
|
html: render(template),
|
||||||
});
|
text: render(template, { plainText: true }),
|
||||||
|
});
|
||||||
|
|
||||||
await prisma.recipient.update({
|
await tx.recipient.update({
|
||||||
where: {
|
where: {
|
||||||
id: recipient.id,
|
id: recipient.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
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,34 +1,76 @@
|
|||||||
'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: {
|
where: {
|
||||||
id: documentId,
|
id: userId,
|
||||||
OR: [
|
},
|
||||||
{
|
});
|
||||||
userId,
|
|
||||||
},
|
return await prisma.$transaction(async (tx) => {
|
||||||
{
|
const document = await tx.document.findFirstOrThrow({
|
||||||
team: {
|
where: {
|
||||||
members: {
|
id: documentId,
|
||||||
some: {
|
OR: [
|
||||||
userId,
|
{
|
||||||
|
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 { 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,16 +17,38 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient || !recipient.documentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.recipient.update({
|
const { documentId } = recipient;
|
||||||
where: {
|
|
||||||
id: recipient.id,
|
await prisma.$transaction(async (tx) => {
|
||||||
},
|
await tx.recipient.update({
|
||||||
data: {
|
where: {
|
||||||
readStatus: ReadStatus.OPENED,
|
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';
|
'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,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) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.completedAt) {
|
||||||
|
throw new Error('Document already complete');
|
||||||
|
}
|
||||||
|
|
||||||
const existingFields = await prisma.field.findMany({
|
const existingFields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
@ -75,56 +98,123 @@ 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({
|
|
||||||
where: {
|
const upsertedField = await tx.field.upsert({
|
||||||
id: field._persisted?.id ?? -1,
|
where: {
|
||||||
documentId,
|
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,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Recipient: {
|
update: {
|
||||||
connect: {
|
page: field.pageNumber,
|
||||||
documentId_email: {
|
positionX: field.pageX,
|
||||||
documentId,
|
positionY: field.pageY,
|
||||||
email: field.signerEmail.toLowerCase(),
|
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) {
|
if (removedFields.length > 0) {
|
||||||
await prisma.field.deleteMany({
|
await prisma.$transaction(async (tx) => {
|
||||||
where: {
|
await tx.field.deleteMany({
|
||||||
id: {
|
where: {
|
||||||
in: removedFields.map((field) => field.id),
|
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';
|
'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,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) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.completedAt) {
|
||||||
|
throw new Error('Document already complete');
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedRecipients = recipients.map((recipient) => ({
|
const normalizedRecipients = recipients.map((recipient) => ({
|
||||||
...recipient,
|
...recipient,
|
||||||
email: recipient.email.toLowerCase(),
|
email: recipient.email.toLowerCase(),
|
||||||
@ -77,49 +99,127 @@ export const setRecipientsForDocument = async ({
|
|||||||
})
|
})
|
||||||
.filter((recipient) => {
|
.filter((recipient) => {
|
||||||
return (
|
return (
|
||||||
recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
recipient._persisted?.role === RecipientRole.CC ||
|
||||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
|
(recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||||
|
recipient._persisted?.signingStatus !== SigningStatus.SIGNED)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
},
|
||||||
},
|
update: {
|
||||||
update: {
|
name: recipient.name,
|
||||||
name: recipient.name,
|
email: recipient.email,
|
||||||
email: recipient.email,
|
role: recipient.role,
|
||||||
role: recipient.role,
|
documentId,
|
||||||
documentId,
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||||
signingStatus:
|
signingStatus:
|
||||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
role: recipient.role,
|
role: recipient.role,
|
||||||
token: nanoid(),
|
token: nanoid(),
|
||||||
documentId,
|
documentId,
|
||||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||||
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) => {
|
||||||
where: {
|
await tx.recipient.deleteMany({
|
||||||
id: {
|
where: {
|
||||||
in: removedRecipients.map((recipient) => recipient.id),
|
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 = (
|
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,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta" ADD COLUMN "redirectUrl" TEXT;
|
||||||
@ -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
|
||||||
@ -199,6 +218,7 @@ model DocumentMeta {
|
|||||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||||
documentId Int @unique
|
documentId Int @unique
|
||||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
|
redirectUrl String?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReadStatus {
|
enum ReadStatus {
|
||||||
@ -259,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);
|
||||||
@ -215,20 +221,23 @@ export const documentRouter = router({
|
|||||||
try {
|
try {
|
||||||
const { documentId, meta } = input;
|
const { documentId, meta } = input;
|
||||||
|
|
||||||
if (meta.message || meta.subject || meta.timezone || meta.dateFormat) {
|
if (meta.message || meta.subject || meta.timezone || meta.dateFormat || meta.redirectUrl) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId,
|
documentId,
|
||||||
subject: meta.subject,
|
subject: meta.subject,
|
||||||
message: meta.message,
|
message: meta.message,
|
||||||
dateFormat: meta.dateFormat,
|
dateFormat: meta.dateFormat,
|
||||||
timezone: meta.timezone,
|
timezone: meta.timezone,
|
||||||
|
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);
|
||||||
@ -247,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);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||||
@ -73,6 +74,12 @@ export const ZSendDocumentMutationSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
dateFormat: z.string(),
|
dateFormat: z.string(),
|
||||||
|
redirectUrl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((value) => value === undefined || URL_REGEX.test(value), {
|
||||||
|
message: 'Please enter a valid URL',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -63,6 +63,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,
|
||||||
|
|||||||
@ -304,6 +304,13 @@ export const AddFieldsFormPartial = ({
|
|||||||
return recipientsByRole;
|
return recipientsByRole;
|
||||||
}, [recipients]);
|
}, [recipients]);
|
||||||
|
|
||||||
|
const recipientsByRoleToDisplay = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
|
||||||
|
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
|
||||||
|
);
|
||||||
|
}, [recipientsByRole]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
@ -382,13 +389,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
</span>
|
</span>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
{Object.entries(recipientsByRole).map(([role, recipients], roleIndex) => (
|
{recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
|
||||||
<CommandGroup key={roleIndex}>
|
<CommandGroup key={roleIndex}>
|
||||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||||
{
|
{`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
RECIPIENT_ROLES_DESCRIPTION[role as RecipientRole].roleName
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{recipients.length === 0 && (
|
{recipients.length === 0 && (
|
||||||
@ -403,7 +407,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
{recipients.map((recipient) => (
|
{recipients.map((recipient) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
className={cn('px-4 last:mb-1 [&:not(:first-child)]:mt-1', {
|
className={cn('px-2 last:mb-1 [&:not(:first-child)]:mt-1', {
|
||||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||||
})}
|
})}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@ -413,7 +417,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn('text-foreground/70 truncate', {
|
className={cn('text-foreground/70 truncate', {
|
||||||
'text-foreground': recipient === selectedSigner,
|
'text-foreground/80': recipient === selectedSigner,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{recipient.name && (
|
{recipient.name && (
|
||||||
|
|||||||
@ -105,7 +105,10 @@ export const AddSignersFormPartial = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return recipients.some(
|
return recipients.some(
|
||||||
(recipient) => recipient.id === id && recipient.sendStatus === SendStatus.SENT,
|
(recipient) =>
|
||||||
|
recipient.id === id &&
|
||||||
|
recipient.sendStatus === SendStatus.SENT &&
|
||||||
|
recipient.role !== RecipientRole.CC,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
@ -23,6 +25,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { Combobox } from '../combobox';
|
import { Combobox } from '../combobox';
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
@ -30,7 +33,7 @@ import { Input } from '../input';
|
|||||||
import { Label } from '../label';
|
import { Label } from '../label';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
import { Textarea } from '../textarea';
|
import { Textarea } from '../textarea';
|
||||||
import type { TAddSubjectFormSchema } from './add-subject.types';
|
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
@ -69,8 +72,10 @@ export const AddSubjectFormPartial = ({
|
|||||||
message: document.documentMeta?.message ?? '',
|
message: document.documentMeta?.message ?? '',
|
||||||
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
redirectUrl: document.documentMeta?.redirectUrl ?? '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolver: zodResolver(ZAddSubjectFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
@ -163,64 +168,94 @@ export const AddSubjectFormPartial = ({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasDateField && (
|
<Accordion type="multiple" className="mt-8 border-none">
|
||||||
<Accordion type="multiple" className="mt-8 border-none">
|
<AccordionItem value="advanced-options" className="border-none">
|
||||||
<AccordionItem value="advanced-options" className="border-none">
|
<AccordionTrigger className="mb-2 border-b text-left hover:no-underline">
|
||||||
<AccordionTrigger className="mb-2 border-b text-left hover:no-underline">
|
Advanced Options
|
||||||
Advanced Options
|
</AccordionTrigger>
|
||||||
</AccordionTrigger>
|
|
||||||
|
|
||||||
<AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 text-sm leading-relaxed">
|
<AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 pt-2 text-sm leading-relaxed">
|
||||||
<div className="mt-2 flex flex-col">
|
{hasDateField && (
|
||||||
<Label htmlFor="date-format">
|
<>
|
||||||
Date Format <span className="text-muted-foreground">(Optional)</span>
|
<div className="flex flex-col">
|
||||||
</Label>
|
<Label htmlFor="date-format">
|
||||||
|
Date Format <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`meta.dateFormat`}
|
name={`meta.dateFormat`}
|
||||||
disabled={documentHasBeenSent}
|
disabled={documentHasBeenSent}
|
||||||
render={({ field: { value, onChange, disabled } }) => (
|
render={({ field: { value, onChange, disabled } }) => (
|
||||||
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
||||||
<SelectTrigger className="bg-background mt-2">
|
<SelectTrigger className="bg-background mt-2">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{DATE_FORMATS.map((format) => (
|
{DATE_FORMATS.map((format) => (
|
||||||
<SelectItem key={format.key} value={format.value}>
|
<SelectItem key={format.key} value={format.value}>
|
||||||
{format.label}
|
{format.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col">
|
||||||
|
<Label htmlFor="time-zone">
|
||||||
|
Time Zone <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`meta.timezone`}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Combobox
|
||||||
|
className="bg-background"
|
||||||
|
options={TIME_ZONES}
|
||||||
|
value={value}
|
||||||
|
onChange={(value) => value && onChange(value)}
|
||||||
|
disabled={documentHasBeenSent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="redirectUrl" className="flex items-center">
|
||||||
|
Redirect URL{' '}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="mx-2 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||||
|
Add a URL to redirect the user to once the document is signed
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="redirectUrl"
|
||||||
|
type="url"
|
||||||
|
className="bg-background my-2"
|
||||||
|
{...register('meta.redirectUrl')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormErrorMessage className="mt-2" error={errors.meta?.redirectUrl} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-4 flex flex-col">
|
</AccordionContent>
|
||||||
<Label htmlFor="time-zone">
|
</AccordionItem>
|
||||||
Time Zone <span className="text-muted-foreground">(Optional)</span>
|
</Accordion>
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`meta.timezone`}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Combobox
|
|
||||||
className="bg-background"
|
|
||||||
options={TIME_ZONES}
|
|
||||||
value={value}
|
|
||||||
onChange={(value) => value && onChange(value)}
|
|
||||||
disabled={documentHasBeenSent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
|
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||||
|
|
||||||
export const ZAddSubjectFormSchema = z.object({
|
export const ZAddSubjectFormSchema = z.object({
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
@ -9,6 +10,12 @@ export const ZAddSubjectFormSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
|
redirectUrl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((value) => value === undefined || URL_REGEX.test(value), {
|
||||||
|
message: 'Please enter a valid URL',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user