Files
documenso/packages/trpc/server/document-router/download-document-certificate.ts
T
Lucas Smith 241929bb97 feat: add API endpoints for downloading certificate and audit log PDFs (#3025)
Add V2 API routes to download an envelope's certificate and audit log
separately, and align the internal cert/audit log downloads to use
envelopeId.

Enforces document visibility via getEnvelopeWhereInput and loads field
signatures so certificates render correctly.
2026-06-26 00:01:55 +10:00

89 lines
2.6 KiB
TypeScript

import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { authenticatedProcedure } from '../trpc';
import {
ZDownloadDocumentCertificateRequestSchema,
ZDownloadDocumentCertificateResponseSchema,
} from './download-document-certificate.types';
export const downloadDocumentCertificateRoute = authenticatedProcedure
.input(ZDownloadDocumentCertificateRequestSchema)
.output(ZDownloadDocumentCertificateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: EnvelopeType.DOCUMENT,
userId: ctx.user.id,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: {
include: {
signature: true,
},
},
documentMeta: true,
user: {
select: {
email: true,
name: true,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
// A cancelled document was never sealed/completed, so a signing certificate
// must not be generated for it. REJECTED and COMPLETED keep their prior behavior.
if (!isDocumentCompleted(envelope.status) || envelope.status === DocumentStatus.CANCELLED) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const certificatePdf = await generateCertificatePdf({
envelope,
recipients: envelope.recipients,
fields: envelope.fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height,
});
const result = await certificatePdf.save();
return {
data: Buffer.from(result).toString('base64'),
envelopeTitle: envelope.title,
};
});