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 = ({