diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
index e20b94887..7082c04e1 100644
--- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
+++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx
@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { base64 } from '@documenso/lib/universal/base64';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentDataType, Prisma } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
@@ -115,7 +115,7 @@ export const SinglePlayerClient = () => {
}
try {
- const putFileData = await putFile(uploadedFile.file);
+ const putFileData = await putPdfFile(uploadedFile.file);
const documentToken = await createSinglePlayerDocument({
documentData: {
diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx
index 26f1e795c..0a0c029ab 100644
--- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx
@@ -10,8 +10,9 @@ import { useSession } from 'next-auth/react';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
+import { AppError } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
@@ -57,7 +58,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
try {
setIsLoading(true);
- const { type, data } = await putFile(file);
+ const { type, data } = await putPdfFile(file);
const { id: documentDataId } = await createDocumentData({
type,
@@ -83,13 +84,21 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
});
router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
- } catch (error) {
- console.error(error);
+ } catch (err) {
+ const error = AppError.parseError(err);
- if (error instanceof TRPCClientError) {
+ console.error(err);
+
+ if (error.code === 'INVALID_DOCUMENT_FILE') {
+ toast({
+ title: 'Invalid file',
+ description: 'You cannot upload encrypted PDFs',
+ variant: 'destructive',
+ });
+ } else if (err instanceof TRPCClientError) {
toast({
title: 'Error',
- description: error.message,
+ description: err.message,
variant: 'destructive',
});
} else {
diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx
index ab05ac3dc..1a6e34584 100644
--- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx
+++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx
@@ -12,7 +12,7 @@ import * as z from 'zod';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { base64 } from '@documenso/lib/universal/base64';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -98,7 +98,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
const file: File = uploadedFile.file;
try {
- const { type, data } = await putFile(file);
+ const { type, data } = await putPdfFile(file);
const { id: templateDocumentDataId } = await createDocumentData({
type,
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/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
index 253803fc8..b1e069e35 100644
--- a/packages/api/v1/implementation.ts
+++ b/packages/api/v1/implementation.ts
@@ -22,7 +22,7 @@ import { updateRecipient } from '@documenso/lib/server-only/recipient/update-rec
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { getFile } from '@documenso/lib/universal/upload/get-file';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import {
getPresignGetUrl,
getPresignPostUrl,
@@ -303,7 +303,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
formValues: body.formValues,
});
- const newDocumentData = await putFile({
+ const newDocumentData = await putPdfFile({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
diff --git a/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts b/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts
index 22f60069e..cda583e81 100644
--- a/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts
+++ b/packages/ee/server-only/stripe/webhook/on-early-adopters-checkout.ts
@@ -5,7 +5,7 @@ import { sealDocument } from '@documenso/lib/server-only/document/seal-document'
import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe';
import { alphaid, nanoid } from '@documenso/lib/universal/id';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
@@ -74,7 +74,7 @@ export const onEarlyAdoptersCheckout = async ({ session }: OnEarlyAdoptersChecko
new URL('@documenso/assets/documenso-supporter-pledge.pdf', import.meta.url),
).then(async (res) => res.arrayBuffer());
- const { id: documentDataId } = await putFile({
+ const { id: documentDataId } = await putPdfFile({
name: 'Documenso Supporter Pledge.pdf',
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(documentBuffer),
diff --git a/packages/lib/constants/feature-flags.ts b/packages/lib/constants/feature-flags.ts
index 83137217f..9533f4d18 100644
--- a/packages/lib/constants/feature-flags.ts
+++ b/packages/lib/constants/feature-flags.ts
@@ -21,6 +21,7 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
* Does not take any person or group properties into account.
*/
export const LOCAL_FEATURE_FLAGS: Record = {
+ app_allow_encrypted_documents: false,
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
app_document_page_view_history_sheet: false,
app_passkey: WEBAPP_BASE_URL === 'http://localhost:3000', // Temp feature flag.
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..0546d96e3 100644
--- a/packages/lib/server-only/document/seal-document.ts
+++ b/packages/lib/server-only/document/seal-document.ts
@@ -14,7 +14,7 @@ import { signPdf } from '@documenso/signing';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
-import { putFile } from '../../universal/upload/put-file';
+import { putPdfFile } from '../../universal/upload/put-file';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
@@ -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);
@@ -119,7 +122,7 @@ export const sealDocument = async ({
const { name, ext } = path.parse(document.title);
- const { data: newData } = await putFile({
+ const { data: newData } = await putPdfFile({
name: `${name}_signed${ext}`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBuffer),
@@ -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 b3ea71532..f05a41db1 100644
--- a/packages/lib/server-only/document/send-document.tsx
+++ b/packages/lib/server-only/document/send-document.tsx
@@ -8,6 +8,7 @@ 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 { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
@@ -20,7 +21,6 @@ import {
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../constants/recipient-roles';
import { getFile } from '../../universal/upload/get-file';
-import { putFile } from '../../universal/upload/put-file';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -102,7 +102,7 @@ export const sendDocument = async ({
formValues: document.formValues as Record,
});
- const newDocumentData = await putFile({
+ const newDocumentData = await putPdfFile({
name: document.title,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
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/upload/put-file.ts b/packages/lib/universal/upload/put-file.ts
index b6aab5f11..06ecd68ff 100644
--- a/packages/lib/universal/upload/put-file.ts
+++ b/packages/lib/universal/upload/put-file.ts
@@ -1,9 +1,12 @@
import { base64 } from '@scure/base';
import { env } from 'next-runtime-env';
+import { PDFDocument } from 'pdf-lib';
import { match } from 'ts-pattern';
+import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { DocumentDataType } from '@documenso/prisma/client';
+import { AppError } from '../../errors/app-error';
import { createDocumentData } from '../../server-only/document-data/create-document-data';
type File = {
@@ -12,14 +15,38 @@ type File = {
arrayBuffer: () => Promise;
};
+/**
+ * Uploads a document file to the appropriate storage location and creates
+ * a document data record.
+ */
+export const putPdfFile = async (file: File) => {
+ const isEncryptedDocumentsAllowed = await getFlag('app_allow_encrypted_documents').catch(
+ () => false,
+ );
+
+ // This will prevent uploading encrypted PDFs or anything that can't be opened.
+ if (!isEncryptedDocumentsAllowed) {
+ await PDFDocument.load(await file.arrayBuffer()).catch((e) => {
+ console.error(`PDF upload parse error: ${e.message}`);
+
+ throw new AppError('INVALID_DOCUMENT_FILE');
+ });
+ }
+
+ const { type, data } = await putFile(file);
+
+ return await createDocumentData({ type, data });
+};
+
+/**
+ * Uploads a file to the appropriate storage location.
+ */
export const putFile = async (file: File) => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
- const { type, data } = await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
+ return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file))
.otherwise(async () => putFileInDatabase(file));
-
- return await createDocumentData({ type, data });
};
const putFileInDatabase = async (file: File) => {
diff --git a/packages/trpc/server/singleplayer-router/router.ts b/packages/trpc/server/singleplayer-router/router.ts
index 33b125110..2634ca895 100644
--- a/packages/trpc/server/singleplayer-router/router.ts
+++ b/packages/trpc/server/singleplayer-router/router.ts
@@ -10,7 +10,7 @@ import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/cons
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { prisma } from '@documenso/prisma';
import {
DocumentStatus,
@@ -86,7 +86,7 @@ export const singleplayerRouter = router({
},
});
- const { id: documentDataId } = await putFile({
+ const { id: documentDataId } = await putPdfFile({
name: `${documentName}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(signedPdfBuffer),
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 = ({