diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx index a477d75c6..2593ef0b8 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx @@ -14,6 +14,12 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client'; import { trpc as trpcClient } from '@documenso/trpc/client'; import { Button } from '@documenso/ui/primitives/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; import { useToast } from '@documenso/ui/primitives/use-toast'; export type DocumentPageViewButtonProps = { @@ -44,11 +50,19 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps const documentsPath = formatDocumentsPath(document.team?.url); - const onDownloadClick = async () => { + const onDownloadClick = async ({ + includeCertificate = true, + includeAuditLog = true, + }: { + includeCertificate?: boolean; + includeAuditLog?: boolean; + } = {}) => { try { const documentWithData = await trpcClient.document.getDocumentById.query( { documentId: document.id, + includeCertificate, + includeAuditLog, }, { context: { @@ -63,7 +77,12 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps throw new Error('No document available'); } - await downloadPDF({ documentData, fileName: documentWithData.title }); + await downloadPDF({ + documentData, + fileName: documentWithData.title, + includeCertificate, + includeAuditLog, + }); } catch (err) { toast({ title: _(msg`Something went wrong`), @@ -112,10 +131,44 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps )) .with({ isComplete: true }, () => ( - + + + + + + + void onDownloadClick()}> + Complete Document + + + + void onDownloadClick({ includeCertificate: true, includeAuditLog: false }) + } + > + Without Audit Log + + + + void onDownloadClick({ includeCertificate: false, includeAuditLog: true }) + } + > + Without Certificate + + + + void onDownloadClick({ includeCertificate: false, includeAuditLog: false }) + } + > + Without Certificate & Audit Log + + + )) .otherwise(() => null); }; diff --git a/packages/lib/client-only/download-pdf.ts b/packages/lib/client-only/download-pdf.ts index 830e3428a..197262f5c 100644 --- a/packages/lib/client-only/download-pdf.ts +++ b/packages/lib/client-only/download-pdf.ts @@ -6,9 +6,16 @@ import { downloadFile } from './download-file'; type DownloadPDFProps = { documentData: DocumentData; fileName?: string; + includeCertificate?: boolean; + includeAuditLog?: boolean; }; -export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) => { +export const downloadPDF = async ({ + documentData, + fileName, + includeCertificate, + includeAuditLog, +}: DownloadPDFProps) => { const bytes = await getFile(documentData); const blob = new Blob([bytes], { @@ -17,8 +24,18 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, ''); + let suffix = '_signed'; + + if (includeCertificate && includeAuditLog) { + suffix = suffix + '_with_certificate_and_audit'; + } else if (includeCertificate) { + suffix = suffix + '_with_certificate'; + } else if (includeAuditLog) { + suffix = suffix + '_with_audit'; + } + downloadFile({ - filename: `${baseTitle}_signed.pdf`, + filename: `${baseTitle}${suffix}.pdf`, data: blob, }); }; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 2a151b7c1..ada909fce 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -1,5 +1,6 @@ import { TRPCError } from '@trpc/server'; import { DateTime } from 'luxon'; +import { PDFDocument } from 'pdf-lib'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; @@ -23,6 +24,7 @@ import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/ import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client'; @@ -65,13 +67,84 @@ export const documentRouter = router({ .input(ZGetDocumentByIdQuerySchema) .query(async ({ input, ctx }) => { const { teamId } = ctx; - const { documentId } = input; + const { documentId, includeCertificate, includeAuditLog } = input; - return await getDocumentById({ + const documentWithData = await getDocumentById({ userId: ctx.user.id, teamId, documentId, }); + + if (includeCertificate && includeAuditLog) { + return documentWithData; + } else if (includeCertificate) { + const pdfData = await getFile(documentWithData.documentData); + const pdfDoc = await PDFDocument.load(pdfData); + + const totalPages = pdfDoc.getPageCount(); + + if (!includeAuditLog) { + pdfDoc.removePage(totalPages - 1); + } + + const pdfBytes = await pdfDoc.save(); + const pdfBuffer = Buffer.from(pdfBytes).toString('base64'); + + return { + ...documentWithData, + documentData: { + ...documentWithData.documentData, + data: pdfBuffer, + initialData: documentWithData.documentData.data, + type: DocumentDataType.BYTES_64, + }, + }; + } else if (includeAuditLog) { + const pdfData = await getFile(documentWithData.documentData); + const pdfDoc = await PDFDocument.load(pdfData); + + const totalPages = pdfDoc.getPageCount(); + + if (!includeCertificate) { + pdfDoc.removePage(totalPages - 2); + } + + const pdfBytes = await pdfDoc.save(); + const pdfBuffer = Buffer.from(pdfBytes).toString('base64'); + + return { + ...documentWithData, + documentData: { + ...documentWithData.documentData, + data: pdfBuffer, + initialData: documentWithData.documentData.data, + type: DocumentDataType.BYTES_64, + }, + }; + } else if (!includeCertificate && !includeAuditLog) { + const pdfData = await getFile(documentWithData.documentData); + const pdfDoc = await PDFDocument.load(pdfData); + + const totalPages = pdfDoc.getPageCount(); + + pdfDoc.removePage(totalPages - 1); + pdfDoc.removePage(totalPages - 2); + + const pdfBytes = await pdfDoc.save(); + const pdfBuffer = Buffer.from(pdfBytes).toString('base64'); + + return { + ...documentWithData, + documentData: { + ...documentWithData.documentData, + data: pdfBuffer, + initialData: documentWithData.documentData.data, + type: DocumentDataType.BYTES_64, + }, + }; + } else { + return documentWithData; + } }), /** diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 887bc48e2..4211d3410 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -141,6 +141,8 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend( export const ZGetDocumentByIdQuerySchema = z.object({ documentId: z.number(), + includeCertificate: z.boolean().default(true).optional(), + includeAuditLog: z.boolean().default(true).optional(), }); export const ZDuplicateDocumentRequestSchema = z.object({