diff --git a/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx b/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx
index 878332f35..c56f53702 100644
--- a/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx
@@ -5,7 +5,7 @@ import { Button } from '@documenso/ui/primitives/button';
export default function SignatureDisclosure() {
return (
-
+
Electronic Signature Disclosure
Welcome
diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts
index 365b6ec40..31f6e9ea3 100644
--- a/apps/web/src/pages/api/auth/[...nextauth].ts
+++ b/apps/web/src/pages/api/auth/[...nextauth].ts
@@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import NextAuth from 'next-auth';
+import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
@@ -18,15 +20,29 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
error: '/signin',
},
events: {
- signIn: async ({ user }) => {
- await prisma.userSecurityAuditLog.create({
- data: {
- userId: user.id,
- ipAddress,
- userAgent,
- type: UserSecurityAuditLogType.SIGN_IN,
- },
- });
+ signIn: async ({ user: { id: userId } }) => {
+ const [user] = await Promise.all([
+ await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ }),
+ await prisma.userSecurityAuditLog.create({
+ data: {
+ userId,
+ ipAddress,
+ userAgent,
+ type: UserSecurityAuditLogType.SIGN_IN,
+ },
+ }),
+ ]);
+
+ // Create the Stripe customer and attach it to the user if it doesn't exist.
+ if (user.customerId === null && IS_BILLING_ENABLED()) {
+ await getStripeCustomerByUser(user).catch((err) => {
+ console.error(err);
+ });
+ }
},
signOut: async ({ token }) => {
const userId = typeof token.id === 'string' ? parseInt(token.id) : token.id;
diff --git a/apps/web/src/pages/api/trpc/[trpc].ts b/apps/web/src/pages/api/trpc/[trpc].ts
index c43291ea1..ba79244b5 100644
--- a/apps/web/src/pages/api/trpc/[trpc].ts
+++ b/apps/web/src/pages/api/trpc/[trpc].ts
@@ -3,7 +3,7 @@ import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router';
export const config = {
- maxDuration: 60,
+ maxDuration: 120,
api: {
bodyParser: {
sizeLimit: '50mb',
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
index 8ee0350bd..253803fc8 100644
--- a/packages/api/v1/implementation.ts
+++ b/packages/api/v1/implementation.ts
@@ -229,6 +229,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
requestMetadata: extractNextApiRequestMetadata(args.req),
});
+ await upsertDocumentMeta({
+ documentId: document.id,
+ userId: user.id,
+ ...body.meta,
+ requestMetadata: extractNextApiRequestMetadata(args.req),
+ });
+
const recipients = await setRecipientsForDocument({
userId: user.id,
teamId: team?.id,
@@ -324,10 +331,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
- subject: body.meta.subject,
- message: body.meta.message,
- dateFormat: body.meta.dateFormat,
- timezone: body.meta.timezone,
+ ...body.meta,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts
index d16b83ea1..29d17cc50 100644
--- a/packages/lib/server-only/document/complete-document-with-token.ts
+++ b/packages/lib/server-only/document/complete-document-with-token.ts
@@ -137,7 +137,7 @@ export const completeDocumentWithToken = async ({
await sendPendingEmail({ documentId, recipientId: recipient.id });
}
- const documents = await prisma.document.updateMany({
+ const haveAllRecipientsSigned = await prisma.document.findFirst({
where: {
id: document.id,
Recipient: {
@@ -146,13 +146,9 @@ export const completeDocumentWithToken = async ({
},
},
},
- data: {
- status: DocumentStatus.COMPLETED,
- completedAt: new Date(),
- },
});
- if (documents.count > 0) {
+ if (haveAllRecipientsSigned) {
await sealDocument({ documentId: document.id, requestMetadata });
}
diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts
index a097d76e9..bf4f8aa06 100644
--- a/packages/lib/server-only/document/delete-document.ts
+++ b/packages/lib/server-only/document/delete-document.ts
@@ -75,18 +75,20 @@ export const deleteDocument = async ({
}
// Continue to hide the document from the user if they are a recipient.
+ // Dirty way of doing this but it's faster than refetching the document.
if (userRecipient?.documentDeletedAt === null) {
- await prisma.recipient.update({
- where: {
- documentId_email: {
- documentId: document.id,
- email: user.email,
+ await prisma.recipient
+ .update({
+ where: {
+ id: userRecipient.id,
},
- },
- data: {
- documentDeletedAt: new Date().toISOString(),
- },
- });
+ data: {
+ documentDeletedAt: new Date().toISOString(),
+ },
+ })
+ .catch(() => {
+ // Do nothing.
+ });
}
// Return partial document for API v1 response.
diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts
index 3e366dc81..564dfc049 100644
--- a/packages/lib/server-only/document/seal-document.ts
+++ b/packages/lib/server-only/document/seal-document.ts
@@ -40,6 +40,11 @@ export const sealDocument = async ({
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
+ Recipient: {
+ every: {
+ signingStatus: SigningStatus.SIGNED,
+ },
+ },
},
include: {
documentData: true,
@@ -53,10 +58,6 @@ export const sealDocument = async ({
throw new Error(`Document ${document.id} has no document data`);
}
- if (document.status !== DocumentStatus.COMPLETED) {
- throw new Error(`Document ${document.id} has not been completed`);
- }
-
const recipients = await prisma.recipient.findMany({
where: {
documentId: document.id,
@@ -92,9 +93,9 @@ export const sealDocument = async ({
// !: Need to write the fields onto the document as a hard copy
const pdfData = await getFile(documentData);
- const certificate = await getCertificatePdf({ documentId }).then(async (doc) =>
- PDFDocument.load(doc),
- );
+ const certificate = await getCertificatePdf({ documentId })
+ .then(async (doc) => PDFDocument.load(doc))
+ .catch(() => null);
const doc = await PDFDocument.load(pdfData);
@@ -103,11 +104,13 @@ export const sealDocument = async ({
doc.getForm().flatten();
flattenAnnotations(doc);
- const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
+ if (certificate) {
+ const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
- certificatePages.forEach((page) => {
- doc.addPage(page);
- });
+ certificatePages.forEach((page) => {
+ doc.addPage(page);
+ });
+ }
for (const field of fields) {
await insertFieldInPDF(doc, field);
@@ -138,6 +141,16 @@ export const sealDocument = async ({
}
await prisma.$transaction(async (tx) => {
+ await tx.document.update({
+ where: {
+ id: document.id,
+ },
+ data: {
+ status: DocumentStatus.COMPLETED,
+ completedAt: new Date(),
+ },
+ });
+
await tx.documentData.update({
where: {
id: documentData.id,
diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx
index 5bb7e2352..9f68ed29b 100644
--- a/packages/lib/server-only/document/send-document.tsx
+++ b/packages/lib/server-only/document/send-document.tsx
@@ -4,6 +4,8 @@ import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
+import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
+import { updateDocument } from '@documenso/lib/server-only/document/update-document';
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';
@@ -211,6 +213,31 @@ export const sendDocument = async ({
}),
);
+ const allRecipientsHaveNoActionToTake = document.Recipient.every(
+ (recipient) => recipient.role === RecipientRole.CC,
+ );
+
+ if (allRecipientsHaveNoActionToTake) {
+ const updatedDocument = await updateDocument({
+ documentId,
+ userId,
+ teamId,
+ data: { status: DocumentStatus.COMPLETED },
+ });
+
+ await sealDocument({ documentId: updatedDocument.id, requestMetadata });
+
+ // Keep the return type the same for the `sendDocument` method
+ return await prisma.document.findFirstOrThrow({
+ where: {
+ id: documentId,
+ },
+ include: {
+ Recipient: true,
+ },
+ });
+ }
+
const updatedDocument = await prisma.$transaction(async (tx) => {
if (document.status === DocumentStatus.DRAFT) {
await tx.documentAuditLog.create({
diff --git a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
index dee40d41a..1b6150fb9 100644
--- a/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
+++ b/packages/lib/server-only/htmltopdf/get-certificate-pdf.ts
@@ -35,6 +35,7 @@ export const getCertificatePdf = async ({ documentId }: GetCertificatePdfOptions
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, {
waitUntil: 'networkidle',
+ timeout: 10_000,
});
const result = await page.pdf({
diff --git a/packages/lib/universal/get-feature-flag.ts b/packages/lib/universal/get-feature-flag.ts
index f4650f691..92f186ab3 100644
--- a/packages/lib/universal/get-feature-flag.ts
+++ b/packages/lib/universal/get-feature-flag.ts
@@ -17,6 +17,7 @@ export const getFlag = async (
options?: GetFlagOptions,
): Promise => {
const requestHeaders = options?.requestHeaders ?? {};
+ delete requestHeaders['content-length'];
if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS[flag] ?? true;
@@ -25,7 +26,7 @@ export const getFlag = async (
const url = new URL(`${APP_BASE_URL()}/api/feature-flag/get`);
url.searchParams.set('flag', flag);
- const response = await fetch(url, {
+ return await fetch(url, {
headers: {
...requestHeaders,
},
@@ -35,9 +36,10 @@ export const getFlag = async (
})
.then(async (res) => res.json())
.then((res) => ZFeatureFlagValueSchema.parse(res))
- .catch(() => false);
-
- return response;
+ .catch((err) => {
+ console.error(err);
+ return LOCAL_FEATURE_FLAGS[flag] ?? false;
+ });
};
/**
@@ -50,6 +52,7 @@ export const getAllFlags = async (
options?: GetFlagOptions,
): Promise> => {
const requestHeaders = options?.requestHeaders ?? {};
+ delete requestHeaders['content-length'];
if (!isFeatureFlagEnabled()) {
return LOCAL_FEATURE_FLAGS;
@@ -67,7 +70,10 @@ export const getAllFlags = async (
})
.then(async (res) => res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
- .catch(() => LOCAL_FEATURE_FLAGS);
+ .catch((err) => {
+ console.error(err);
+ return LOCAL_FEATURE_FLAGS;
+ });
};
/**
@@ -89,7 +95,10 @@ export const getAllAnonymousFlags = async (): Promise res.json())
.then((res) => z.record(z.string(), ZFeatureFlagValueSchema).parse(res))
- .catch(() => LOCAL_FEATURE_FLAGS);
+ .catch((err) => {
+ console.error(err);
+ return LOCAL_FEATURE_FLAGS;
+ });
};
interface GetFlagOptions {
diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
index e415f1aac..d285fbe44 100644
--- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
+++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
@@ -282,6 +282,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({