fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-08-14 22:13:19 +00:00
88 changed files with 4791 additions and 1066 deletions

View File

@ -0,0 +1,93 @@
import { DocumentDataType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
import { ZDownloadDocumentRequestSchema, ZDownloadDocumentResponseSchema } from './schema';
export const downloadDocumentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/{documentId}/download-beta',
summary: 'Download document (beta)',
description: 'Get a pre-signed download URL for the original or signed version of a document',
tags: ['Document'],
},
})
.input(ZDownloadDocumentRequestSchema)
.output(ZDownloadDocumentResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId, version } = input;
ctx.logger.info({
input: {
documentId,
version,
},
});
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document downloads are only available when S3 storage is configured.',
});
}
const document = await getDocumentById({
documentId,
userId: user.id,
teamId,
});
if (!document.documentData) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document data not found',
});
}
if (document.documentData.type !== DocumentDataType.S3_PATH) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document is not stored in S3 and cannot be downloaded via URL.',
});
}
if (version === 'signed' && !isDocumentCompleted(document.status)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document is not completed yet.',
});
}
try {
const documentData =
version === 'original'
? document.documentData.initialData || document.documentData.data
: document.documentData.data;
const { url } = await getPresignGetUrl(documentData);
const baseTitle = document.title.replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
return {
downloadUrl: url,
filename,
contentType: 'application/pdf',
};
} catch (error) {
ctx.logger.error({
error,
message: 'Failed to generate download URL',
documentId,
version,
});
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to generate download URL',
});
}
});

View File

@ -1,5 +1,4 @@
import { DocumentDataType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { DateTime } from 'luxon';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
@ -27,6 +26,7 @@ import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-action
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { authenticatedProcedure, procedure, router } from '../trpc';
import { downloadDocumentRoute } from './download-document';
import { findInboxRoute } from './find-inbox';
import { getInboxCountRoute } from './get-inbox-count';
import {
@ -63,6 +63,7 @@ export const documentRouter = router({
getCount: getInboxCountRoute,
},
updateDocument: updateDocumentRoute,
downloadDocument: downloadDocumentRoute,
/**
* @private
@ -636,8 +637,7 @@ export const documentRouter = router({
}).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
throw new TRPCError({
code: 'FORBIDDEN',
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have access to this document.',
});
}

View File

@ -295,7 +295,7 @@ export const ZDistributeDocumentRequestSchema = z.object({
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
@ -346,3 +346,22 @@ export const ZDownloadAuditLogsMutationSchema = z.object({
export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(),
});
export const ZDownloadDocumentRequestSchema = z.object({
documentId: z.number().describe('The ID of the document to download.'),
version: z
.enum(['original', 'signed'])
.describe(
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
)
.default('signed'),
});
export const ZDownloadDocumentResponseSchema = z.object({
downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
filename: z.string().describe('The filename of the PDF file'),
contentType: z.string().describe('MIME type of the file'),
});
export type TDownloadDocumentRequest = z.infer<typeof ZDownloadDocumentRequestSchema>;
export type TDownloadDocumentResponse = z.infer<typeof ZDownloadDocumentResponseSchema>;

View File

@ -62,7 +62,7 @@ export const ZUpdateDocumentRequestSchema = z.object({
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),

View File

@ -54,7 +54,7 @@ export const createSubscriptionRoute = authenticatedProcedure
if (!customerId) {
const customer = await createCustomer({
name: organisation.name,
name: organisation.owner.name || organisation.owner.email,
email: organisation.owner.email,
});

View File

@ -77,7 +77,7 @@ export const manageSubscriptionRoute = authenticatedProcedure
// If the customer ID is still missing create a new customer.
if (!customerId) {
const customer = await createCustomer({
name: organisation.name,
name: organisation.owner.name || organisation.owner.email,
email: organisation.owner.email,
});

View File

@ -1,5 +1,4 @@
import { TRPCError } from '@trpc/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createFolder } from '@documenso/lib/server-only/folder/create-folder';
import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder';
import { findFolders } from '@documenso/lib/server-only/folder/find-folders';
@ -137,8 +136,7 @@ export const folderRouter = router({
type,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Parent folder not found',
});
}
@ -248,8 +246,7 @@ export const folderRouter = router({
type: currentFolder.type,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Parent folder not found',
});
}
@ -294,8 +291,7 @@ export const folderRouter = router({
type: FolderType.DOCUMENT,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found',
});
}
@ -340,8 +336,7 @@ export const folderRouter = router({
type: FolderType.TEMPLATE,
});
} catch (error) {
throw new TRPCError({
code: 'NOT_FOUND',
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found',
});
}

View File

@ -30,6 +30,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@ -117,6 +118,7 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,

View File

@ -19,6 +19,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
documentDateFormat: ZDocumentMetaDateFormatSchema.optional(),
includeSenderDetails: z.boolean().optional(),
includeSigningCertificate: z.boolean().optional(),
includeAuditLog: z.boolean().optional(),
typedSignatureEnabled: z.boolean().optional(),
uploadSignatureEnabled: z.boolean().optional(),
drawSignatureEnabled: z.boolean().optional(),

View File

@ -1,6 +1,7 @@
import { RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import {
ZRecipientAccessAuthTypesSchema,
ZRecipientActionAuthSchema,
@ -186,7 +187,18 @@ export const ZSetTemplateRecipientsRequestSchema = z
recipients: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().toLowerCase().email().min(1),
email: z
.string()
.toLowerCase()
.refine(
(email) => {
return (
isTemplateRecipientEmailPlaceholder(email) ||
z.string().email().safeParse(email).success
);
},
{ message: 'Please enter a valid email address' },
),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@ -196,9 +208,12 @@ export const ZSetTemplateRecipientsRequestSchema = z
})
.refine(
(schema) => {
const emails = schema.recipients.map((recipient) => recipient.email);
// Filter out placeholder emails and only check uniqueness for actual emails
const nonPlaceholderEmails = schema.recipients
.map((recipient) => recipient.email)
.filter((email) => !isTemplateRecipientEmailPlaceholder(email));
return new Set(emails).size === emails.length;
return new Set(nonPlaceholderEmails).size === nonPlaceholderEmails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },

View File

@ -32,6 +32,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@ -110,6 +111,7 @@ export const updateTeamSettingsRoute = authenticatedProcedure
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,

View File

@ -23,6 +23,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
documentDateFormat: ZDocumentMetaDateFormatSchema.nullish(),
includeSenderDetails: z.boolean().nullish(),
includeSigningCertificate: z.boolean().nullish(),
includeAuditLog: z.boolean().nullish(),
typedSignatureEnabled: z.boolean().nullish(),
uploadSignatureEnabled: z.boolean().nullish(),
drawSignatureEnabled: z.boolean().nullish(),

View File

@ -1,6 +1,5 @@
import type { Document } from '@prisma/client';
import { DocumentDataType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@ -556,9 +555,9 @@ export const templateRouter = router({
});
if (csv.length > 4 * 1024 * 1024) {
throw new TRPCError({
code: 'BAD_REQUEST',
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'File size exceeds 4MB limit',
statusCode: 400,
});
}
@ -569,8 +568,7 @@ export const templateRouter = router({
});
if (!template) {
throw new TRPCError({
code: 'NOT_FOUND',
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}

View File

@ -65,7 +65,7 @@ export const ZTemplateMetaUpsertSchema = z.object({
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),