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/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/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 564dfc049..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'; @@ -122,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), diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 9f68ed29b..40cdb9ab5 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/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),