Merge branch 'main' into feat/document-file-conversion

Resolved conflicts:
- create-document-data.ts: merged initialData + originalData/originalMimeType
- files.helpers.ts: kept both imports
- envelope-drop-zone-wrapper.tsx: adopted buildDropzoneRejectionDescription
- create-envelope-items.ts: adopted UNSAFE_createEnvelopeItems
- create-envelope.ts: integrated convertToPdfIfNeeded into createEnvelopeRouteCaller

Extended putPdfFileServerSide to accept { initialData, originalData,
originalMimeType } options; updated seal-document.handler call site.
This commit is contained in:
ephraimduncan
2026-04-20 10:11:35 +00:00
1127 changed files with 107456 additions and 22991 deletions
@@ -4,6 +4,8 @@ import { DateTime } from 'luxon';
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '@documenso/lib/server-only/2fa/email/constants';
import { send2FATokenEmail } from '@documenso/lib/server-only/2fa/email/send-2fa-token-email';
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
import { request2FAEmailRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
@@ -21,6 +23,13 @@ export const accessAuthRequest2FAEmailRoute = procedure
try {
const { token } = input;
const rateLimitResult = await request2FAEmailRateLimit.check({
ip: ctx.metadata.requestMetadata.ipAddress ?? 'unknown',
identifier: token,
});
assertRateLimit(rateLimitResult);
const user = ctx.user;
// Get document and recipient by token
@@ -38,6 +38,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
meta,
folderId,
attachments,
formValues,
} = input;
const { remaining } = await getServerLimits({ userId: user.id, teamId });
@@ -68,6 +69,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
title,
externalId,
visibility,
formValues,
globalAccessAuth,
globalActionAuth,
recipients: (recipients || []).map((recipient) => ({
@@ -27,6 +27,7 @@ import {
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
* @deprecated
*/
export const createDocumentTemporaryMeta: TrpcRouteMeta = {
openapi: {
@@ -36,6 +37,7 @@ export const createDocumentTemporaryMeta: TrpcRouteMeta = {
description:
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
tags: ['Document'],
deprecated: true,
},
};
@@ -98,6 +98,7 @@ export const createDocumentRoute = authenticatedProcedure
visibility,
globalAccessAuth,
globalActionAuth,
formValues,
recipients: (recipients || []).map((recipient) => ({
...recipient,
fields: (recipient.fields || []).map((field) => ({
@@ -18,7 +18,7 @@ import {
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from './schema';
@@ -79,7 +79,7 @@ export const ZCreateDocumentPayloadSchema = z.object({
export const ZCreateDocumentRequestSchema = zodFormData({
payload: zfd.json(ZCreateDocumentPayloadSchema),
file: zfd.file(),
file: zfdFile(),
});
export const ZCreateDocumentResponseSchema = z.object({
@@ -11,6 +11,7 @@ import {
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
} from '@documenso/lib/types/document-meta';
import { zEmail } from '@documenso/lib/utils/zod';
import type { TrpcRouteMeta } from '../trpc';
@@ -36,7 +37,7 @@ export const ZDistributeDocumentRequestSchema = z.object({
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailReplyTo: zEmail().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
@@ -1,11 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
import { authenticatedProcedure } from '../trpc';
import {
@@ -42,12 +40,26 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure
});
}
const encrypted = encryptSecondaryData({
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
const certificatePdf = await generateAuditLogPdf({
envelope,
recipients: envelope.recipients,
fields: envelope.fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
envelopeItems: envelope.envelopeItems.map((item) => item.title),
pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height,
});
const result = await certificatePdf.save();
const base64 = Buffer.from(result).toString('base64');
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
data: base64,
envelopeTitle: envelope.title,
};
});
@@ -5,7 +5,8 @@ export const ZDownloadDocumentAuditLogsRequestSchema = z.object({
});
export const ZDownloadDocumentAuditLogsResponseSchema = z.object({
url: z.string(),
data: z.string(),
envelopeTitle: z.string(),
});
export type TDownloadDocumentAuditLogsRequest = z.infer<
@@ -1,12 +1,11 @@
import { EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
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 { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
@@ -27,7 +26,7 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
},
});
const envelope = await getEnvelopeById({
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'documentId',
id: documentId,
@@ -37,16 +36,54 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
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',
});
}
if (!isDocumentCompleted(envelope.status)) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const encrypted = encryptSecondaryData({
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
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();
const base64 = Buffer.from(result).toString('base64');
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
data: base64,
envelopeTitle: envelope.title,
};
});
@@ -5,7 +5,8 @@ export const ZDownloadDocumentCertificateRequestSchema = z.object({
});
export const ZDownloadDocumentCertificateResponseSchema = z.object({
url: z.string(),
data: z.string(),
envelopeTitle: z.string(),
});
export type TDownloadDocumentCertificateRequest = z.infer<
@@ -1,7 +1,5 @@
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
@@ -30,28 +28,15 @@ export const findDocumentsInternalRoute = authenticatedProcedure
folderId,
} = input;
const getStatOptions: GetStatsInput = {
user,
period,
search: query,
folderId,
};
if (teamId) {
const team = await getTeamById({ userId: user.id, teamId });
getStatOptions.team = {
teamId: team.id,
teamEmail: team.teamEmail?.email,
senderIds,
currentTeamMemberRole: team.currentTeamRole,
currentUserEmail: user.email,
userId: user.id,
};
}
const [stats, documents] = await Promise.all([
getStats(getStatOptions),
getStats({
userId: user.id,
teamId,
period,
search: query,
folderId,
senderIds,
}),
findDocuments({
userId: user.id,
teamId,
@@ -38,6 +38,7 @@ export const findDocumentsRoute = authenticatedProcedure
perPage,
folderId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
useWindowedCount: false,
});
return {
@@ -1,11 +1,13 @@
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
export const DOCUMENT_TITLE_MAX_LENGTH = 255;
export const ZDocumentTitleSchema = z
.string()
.trim()
.min(1)
.max(255)
.max(DOCUMENT_TITLE_MAX_LENGTH)
.describe('The title of the document.');
export const ZDocumentExternalIdSchema = z
@@ -1,7 +1,7 @@
import { z } from 'zod';
export const ZSearchDocumentRequestSchema = z.object({
query: z.string(),
query: z.string().trim().min(1).max(1024),
});
export const ZSearchDocumentResponseSchema = z
@@ -5,14 +5,14 @@ import { authenticatedProcedure } from '../trpc';
import {
ZUpdateDocumentRequestSchema,
ZUpdateDocumentResponseSchema,
updateDocumentMeta,
} from './update-document.types';
import { updateDocumentMeta as updateDocumentTrpcMeta } from './update-document.types';
/**
* Public route.
*/
export const updateDocumentRoute = authenticatedProcedure
.meta(updateDocumentTrpcMeta)
.meta(updateDocumentMeta)
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {