Compare commits

..

1 Commits

Author SHA1 Message Date
bbf44acda3 fix: reauth, etc 2025-11-03 16:16:50 +11:00
145 changed files with 1243 additions and 6569 deletions

View File

@ -6,7 +6,7 @@ import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/cl
import { DownloadIcon, FileTextIcon } from 'lucide-react'; import { DownloadIcon, FileTextIcon } from 'lucide-react';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadFile } from '@documenso/lib/client-only/download-file';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -87,11 +87,17 @@ export const EnvelopeDownloadDialog = ({
})); }));
try { try {
const downloadUrl = token const data = await getFile({
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${envelopeItemId}/download/${version}` type: envelopeItem.documentData.type,
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${envelopeItemId}/download/${version}`; data:
version === 'signed'
? envelopeItem.documentData.data
: envelopeItem.documentData.initialData,
});
const blob = await fetch(downloadUrl).then(async (res) => await res.blob()); const blob = new Blob([data], {
type: 'application/pdf',
});
const baseTitle = envelopeItem.title.replace(/\.pdf$/, ''); const baseTitle = envelopeItem.title.replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf'; const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';

View File

@ -7,9 +7,9 @@ import { FilePlus, Loader } from 'lucide-react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
@ -54,17 +54,13 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
setIsUploadingFile(true); setIsUploadingFile(true);
try { try {
const payload = { const response = await putPdfFile(file);
const { legacyTemplateId: id } = await createTemplate({
title: file.name, title: file.name,
templateDocumentDataId: response.id,
folderId: folderId, folderId: folderId,
} satisfies TCreateTemplatePayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createTemplate(formData);
toast({ toast({
title: _(msg`Template document uploaded`), title: _(msg`Template document uploaded`),

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type { TeamGlobalSettings } from '@prisma/client'; import type { TeamGlobalSettings } from '@prisma/client';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -29,8 +29,6 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { Textarea } from '@documenso/ui/primitives/textarea'; import { Textarea } from '@documenso/ui/primitives/textarea';
import { useOptionalCurrentTeam } from '~/providers/team';
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
@ -70,9 +68,6 @@ export function BrandingPreferencesForm({
}: BrandingPreferencesFormProps) { }: BrandingPreferencesFormProps) {
const { t } = useLingui(); const { t } = useLingui();
const team = useOptionalCurrentTeam();
const organisation = useCurrentOrganisation();
const [previewUrl, setPreviewUrl] = useState<string>(''); const [previewUrl, setPreviewUrl] = useState<string>('');
const [hasLoadedPreview, setHasLoadedPreview] = useState(false); const [hasLoadedPreview, setHasLoadedPreview] = useState(false);
@ -93,13 +88,14 @@ export function BrandingPreferencesForm({
const file = JSON.parse(settings.brandingLogo); const file = JSON.parse(settings.brandingLogo);
if ('type' in file && 'data' in file) { if ('type' in file && 'data' in file) {
const logoUrl = void getFile(file).then((binaryData) => {
context === 'Team' const objectUrl = URL.createObjectURL(new Blob([binaryData]));
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/team/${team?.id}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/organisation/${organisation?.id}`;
setPreviewUrl(logoUrl); setPreviewUrl(objectUrl);
setHasLoadedPreview(true); setHasLoadedPreview(true);
});
return;
} }
} }

View File

@ -205,7 +205,6 @@ export const DocumentSigningPageViewV2 = () => {
<div className="flex flex-col items-center justify-center p-2 sm:mt-4 sm:p-4"> <div className="flex flex-col items-center justify-center p-2 sm:mt-4 sm:p-4">
{currentEnvelopeItem ? ( {currentEnvelopeItem ? (
<PDFViewerKonvaLazy <PDFViewerKonvaLazy
renderer="signing"
key={currentEnvelopeItem.id} key={currentEnvelopeItem.id}
documentDataId={currentEnvelopeItem.documentDataId} documentDataId={currentEnvelopeItem.documentDataId}
customPageRenderer={EnvelopeSignerPageRenderer} customPageRenderer={EnvelopeSignerPageRenderer}

View File

@ -174,7 +174,7 @@ const DocumentCertificateQrV2 = ({
<div className="mt-12 w-full"> <div className="mt-12 w-full">
<EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} /> <EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} />
<PDFViewerKonvaLazy renderer="preview" customPageRenderer={EnvelopeGenericPageRenderer} /> <PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
</div> </div>
</div> </div>
); );

View File

@ -16,9 +16,9 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/l
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -62,18 +62,14 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const response = await putPdfFile(file);
const { legacyDocumentId: id } = await createDocument({
title: file.name, title: file.name,
timezone: userTimezone, documentDataId: response.id,
timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field.
folderId: folderId ?? undefined, folderId: folderId ?? undefined,
} satisfies TCreateDocumentPayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createDocument(formData);
void refreshLimits(); void refreshLimits();

View File

@ -13,9 +13,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
import { import {
@ -73,18 +73,14 @@ export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) =
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const response = await putPdfFile(file);
const { legacyDocumentId: id } = await createDocument({
title: file.name, title: file.name,
documentDataId: response.id,
timezone: userTimezone, timezone: userTimezone,
folderId: folderId ?? undefined, folderId: folderId ?? undefined,
} satisfies TCreateDocumentPayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createDocument(formData);
void refreshLimits(); void refreshLimits();

View File

@ -14,9 +14,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
import { import {
@ -78,24 +78,35 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const result = await Promise.all(
files.map(async (file) => {
try {
const response = await putPdfFile(file);
return {
title: file.name,
documentDataId: response.id,
};
} catch (err) {
console.error(err);
throw new Error('Failed to upload document');
}
}),
);
const envelopeItemsToCreate = result.filter(
(item): item is { title: string; documentDataId: string } => item !== undefined,
);
const { id } = await createEnvelope({
folderId, folderId,
type, type,
title: files[0].name, title: files[0].name,
items: envelopeItemsToCreate,
meta: { meta: {
timezone: userTimezone, timezone: userTimezone,
}, },
} satisfies TCreateEnvelopePayload; }).catch((error) => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
for (const file of files) {
formData.append('files', file);
}
const { id } = await createEnvelope(formData).catch((error) => {
console.error(error); console.error(error);
throw error; throw error;

View File

@ -26,7 +26,7 @@ import { fieldButtonList } from './envelope-editor-fields-drag-drop';
export default function EnvelopeEditorFieldsPageRenderer() { export default function EnvelopeEditorFieldsPageRenderer() {
const { t, i18n } = useLingui(); const { t, i18n } = useLingui();
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor(); const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender(); const { currentEnvelopeItem } = useCurrentEnvelopeRender();
const interactiveTransformer = useRef<Transformer | null>(null); const interactiveTransformer = useRef<Transformer | null>(null);
@ -113,7 +113,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
pageLayer.current?.batchDraw(); pageLayer.current?.batchDraw();
}; };
const unsafeRenderFieldOnLayer = (field: TLocalField) => { const renderFieldOnLayer = (field: TLocalField) => {
if (!pageLayer.current) { if (!pageLayer.current) {
return; return;
} }
@ -159,15 +159,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
fieldGroup.on('dragend', handleResizeOrMove); fieldGroup.on('dragend', handleResizeOrMove);
}; };
const renderFieldOnLayer = (field: TLocalField) => {
try {
unsafeRenderFieldOnLayer(field);
} catch (err) {
console.error(err);
setRenderError(true);
}
};
/** /**
* Initialize the Konva page canvas and all fields and interactions. * Initialize the Konva page canvas and all fields and interactions.
*/ */

View File

@ -137,10 +137,7 @@ export const EnvelopeEditorFieldsPage = () => {
)} )}
{currentEnvelopeItem !== null ? ( {currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy <PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
renderer="editor"
customPageRenderer={EnvelopeEditorFieldsPageRenderer}
/>
) : ( ) : (
<div className="flex flex-col items-center justify-center py-32"> <div className="flex flex-col items-center justify-center py-32">
<FileTextIcon className="text-muted-foreground h-10 w-10" /> <FileTextIcon className="text-muted-foreground h-10 w-10" />

View File

@ -229,6 +229,7 @@ export const EnvelopeEditorSettingsDialog = ({
const emails = emailData?.data || []; const emails = emailData?.data || [];
// Todo: Envelopes this doesn't make sense (look at previous)
const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility); const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility);
const onFormSubmit = async (data: TAddSettingsFormSchema) => { const onFormSubmit = async (data: TAddSettingsFormSchema) => {
@ -241,6 +242,7 @@ export const EnvelopeEditorSettingsDialog = ({
try { try {
await updateEnvelope({ await updateEnvelope({
envelopeId: envelope.id, envelopeId: envelope.id,
envelopeType: envelope.type,
data: { data: {
externalId: data.externalId || null, externalId: data.externalId || null,
visibility: data.visibility, visibility: data.visibility,

View File

@ -142,7 +142,7 @@ export const EnvelopeEditorUploadPage = () => {
const { createdEnvelopeItems } = await createEnvelopeItems({ const { createdEnvelopeItems } = await createEnvelopeItems({
envelopeId: envelope.id, envelopeId: envelope.id,
data: envelopeItemsToCreate, items: envelopeItemsToCreate,
}).catch((error) => { }).catch((error) => {
console.error(error); console.error(error);

View File

@ -12,8 +12,7 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
export default function EnvelopeGenericPageRenderer() { export default function EnvelopeGenericPageRenderer() {
const { i18n } = useLingui(); const { i18n } = useLingui();
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } = const { currentEnvelopeItem, fields, getRecipientColorKey } = useCurrentEnvelopeRender();
useCurrentEnvelopeRender();
const { const {
stage, stage,
@ -38,7 +37,7 @@ export default function EnvelopeGenericPageRenderer() {
[fields, pageContext.pageNumber], [fields, pageContext.pageNumber],
); );
const unsafeRenderFieldOnLayer = (field: TEnvelope['fields'][number]) => { const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => {
if (!pageLayer.current) { if (!pageLayer.current) {
console.error('Layer not loaded yet'); console.error('Layer not loaded yet');
return; return;
@ -67,15 +66,6 @@ export default function EnvelopeGenericPageRenderer() {
}); });
}; };
const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => {
try {
unsafeRenderFieldOnLayer(field);
} catch (err) {
console.error(err);
setRenderError(true);
}
};
/** /**
* Initialize the Konva page canvas and all fields and interactions. * Initialize the Konva page canvas and all fields and interactions.
*/ */

View File

@ -36,7 +36,7 @@ import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-
export default function EnvelopeSignerPageRenderer() { export default function EnvelopeSignerPageRenderer() {
const { t, i18n } = useLingui(); const { t, i18n } = useLingui();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender(); const { currentEnvelopeItem } = useCurrentEnvelopeRender();
const { sessionData } = useOptionalSession(); const { sessionData } = useOptionalSession();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
@ -87,7 +87,7 @@ export default function EnvelopeSignerPageRenderer() {
); );
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]); }, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => { const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
if (!pageLayer.current) { if (!pageLayer.current) {
console.error('Layer not loaded yet'); console.error('Layer not loaded yet');
return; return;
@ -244,7 +244,7 @@ export default function EnvelopeSignerPageRenderer() {
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
await signField(field.id, payload); await signField(field.id, payload); // Todo: Envelopes - Handle errors
} }
if (payload?.value) { if (payload?.value) {
@ -363,15 +363,6 @@ export default function EnvelopeSignerPageRenderer() {
fieldGroup.on('pointerdown', handleFieldGroupClick); fieldGroup.on('pointerdown', handleFieldGroupClick);
}; };
const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
try {
unsafeRenderFieldOnLayer(unparsedField);
} catch (err) {
console.error(err);
setRenderError(true);
}
};
const signField = async ( const signField = async (
fieldId: number, fieldId: number,
payload: TSignEnvelopeFieldValue, payload: TSignEnvelopeFieldValue,
@ -398,7 +389,7 @@ export default function EnvelopeSignerPageRenderer() {
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => { const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
// Render the fields. // Render the fields.
for (const field of localPageFields) { for (const field of localPageFields) {
renderFieldOnLayer(field); renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
} }
currentPageLayer.batchDraw(); currentPageLayer.batchDraw();
@ -414,7 +405,7 @@ export default function EnvelopeSignerPageRenderer() {
localPageFields.forEach((field) => { localPageFields.forEach((field) => {
console.log('Field changed/inserted, rendering on canvas'); console.log('Field changed/inserted, rendering on canvas');
renderFieldOnLayer(field); renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
}); });
pageLayer.current.batchDraw(); pageLayer.current.batchDraw();
@ -432,7 +423,7 @@ export default function EnvelopeSignerPageRenderer() {
pageLayer.current.destroyChildren(); pageLayer.current.destroyChildren();
localPageFields.forEach((field) => { localPageFields.forEach((field) => {
renderFieldOnLayer(field); renderFieldOnLayer(field); // Todo: Envelopes - [CRITICAL] Handle errors which prevent rendering
}); });
pageLayer.current.batchDraw(); pageLayer.current.batchDraw();

View File

@ -10,9 +10,9 @@ import { match } from 'ts-pattern';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -40,17 +40,13 @@ export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZon
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const documentData = await putPdfFile(file);
const { legacyTemplateId: id } = await createTemplate({
title: file.name, title: file.name,
templateDocumentDataId: documentData.id,
folderId: folderId ?? undefined, folderId: folderId ?? undefined,
} satisfies TCreateTemplatePayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createTemplate(formData);
toast({ toast({
title: _(msg`Template uploaded`), title: _(msg`Template uploaded`),

View File

@ -156,10 +156,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
<Card className="rounded-xl before:rounded-xl" gradient> <Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2"> <CardContent className="p-2">
<PDFViewerKonvaLazy <PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
renderer="preview"
customPageRenderer={EnvelopeGenericPageRenderer}
/>
</CardContent> </CardContent>
</Card> </Card>
</EnvelopeRenderProvider> </EnvelopeRenderProvider>

View File

@ -179,10 +179,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
<Card className="rounded-xl before:rounded-xl" gradient> <Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2"> <CardContent className="p-2">
<PDFViewerKonvaLazy <PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
renderer="preview"
customPageRenderer={EnvelopeGenericPageRenderer}
/>
</CardContent> </CardContent>
</Card> </Card>
</EnvelopeRenderProvider> </EnvelopeRenderProvider>

View File

@ -1,6 +1,5 @@
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TFieldCheckbox } from '@documenso/lib/types/field'; import type { TFieldCheckbox } from '@documenso/lib/types/field';
import { parseCheckboxCustomText } from '@documenso/lib/utils/fields'; import { parseCheckboxCustomText } from '@documenso/lib/utils/fields';
@ -45,13 +44,6 @@ export const handleCheckboxFieldClick = async (
let checkedValues: number[] | null = newValues.filter((v) => v.isChecked).map((v) => v.index); let checkedValues: number[] | null = newValues.filter((v) => v.isChecked).map((v) => v.index);
if (checkedValues.length === 0) {
return {
type: FieldType.CHECKBOX,
value: [],
};
}
if (validationRule && validationLength) { if (validationRule && validationLength) {
const checkboxValidationRule = checkboxValidationSigns.find( const checkboxValidationRule = checkboxValidationSigns.find(
(sign) => sign.label === validationRule, (sign) => sign.label === validationRule,
@ -63,34 +55,13 @@ export const handleCheckboxFieldClick = async (
}); });
} }
// Custom logic to make it flow better.
// If "at most" OR "exactly" 1 value then just return the new selected value if exists.
if (
(checkboxValidationRule.value === '=' || checkboxValidationRule.value === '<=') &&
validationLength === 1
) {
return {
type: FieldType.CHECKBOX,
value: [clickedCheckboxIndex],
};
}
const isValid = validateCheckboxLength(
checkedValues.length,
checkboxValidationRule.value,
validationLength,
);
// Only render validation dialog if validation is invalid.
if (!isValid) {
checkedValues = await SignFieldCheckboxDialog.call({ checkedValues = await SignFieldCheckboxDialog.call({
fieldMeta: field.fieldMeta, fieldMeta: field.fieldMeta,
validationRule: checkboxValidationRule.value, validationRule: checkboxValidationRule.value,
validationLength, validationLength,
preselectedIndices: checkedValues, preselectedIndices: currentCheckedIndices,
}); });
} }
}
if (!checkedValues) { if (!checkedValues) {
return null; return null;

View File

@ -1,82 +0,0 @@
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
import { type Context } from 'hono';
import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
import type { HonoEnv } from '../router';
type HandleEnvelopeItemFileRequestOptions = {
title: string;
status: DocumentStatus;
documentData: {
type: DocumentDataType;
data: string;
initialData: string;
};
version: 'signed' | 'original';
isDownload: boolean;
context: Context<HonoEnv>;
};
/**
* Helper function to handle envelope item file requests (both view and download)
*/
export const handleEnvelopeItemFileRequest = async ({
title,
status,
documentData,
version,
isDownload,
context: c,
}: HandleEnvelopeItemFileRequestOptions) => {
const documentDataToUse = version === 'signed' ? documentData.data : documentData.initialData;
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
if (c.req.header('If-None-Match') === etag) {
return c.body(null, 304);
}
const file = await getFileServerSide({
type: documentData.type,
data: documentDataToUse,
}).catch((error) => {
console.error(error);
return null;
});
if (!file) {
return c.json({ error: 'File not found' }, 404);
}
c.header('Content-Type', 'application/pdf');
c.header('Content-Length', file.length.toString());
c.header('ETag', etag);
if (!isDownload) {
if (status === DocumentStatus.COMPLETED) {
c.header('Cache-Control', 'public, max-age=31536000, immutable');
} else {
// Set a tiny 1 minute cache, with must-revalidate to ensure the client always checks for updates.
c.header('Cache-Control', 'public, max-age=60, must-revalidate');
}
}
if (isDownload) {
// Generate filename following the pattern from envelope-download-dialog.tsx
const baseTitle = title.replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
c.header('Content-Disposition', `attachment; filename="${filename}"`);
// For downloads, prevent caching to ensure fresh data
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');
c.header('Pragma', 'no-cache');
c.header('Expires', '0');
}
return c.body(file);
};

View File

@ -1,22 +1,21 @@
import { PDFDocument } from '@cantoo/pdf-lib';
import { sValidator } from '@hono/standard-validator'; import { sValidator } from '@hono/standard-validator';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getTeamById } from '@documenso/lib/server-only/team/get-team'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { putFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import {
import { prisma } from '@documenso/prisma'; getPresignGetUrl,
getPresignPostUrl,
} from '@documenso/lib/universal/upload/server-actions';
import type { HonoEnv } from '../router'; import type { HonoEnv } from '../router';
import { handleEnvelopeItemFileRequest } from './files.helpers';
import { import {
type TGetPresignedGetUrlResponse,
type TGetPresignedPostUrlResponse, type TGetPresignedPostUrlResponse,
ZGetEnvelopeItemFileDownloadRequestParamsSchema, ZGetPresignedGetUrlRequestSchema,
ZGetEnvelopeItemFileRequestParamsSchema,
ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema,
ZGetEnvelopeItemFileTokenRequestParamsSchema,
ZGetPresignedPostUrlRequestSchema, ZGetPresignedPostUrlRequestSchema,
ZUploadPdfRequestSchema, ZUploadPdfRequestSchema,
} from './files.types'; } from './files.types';
@ -43,7 +42,29 @@ export const filesRoute = new Hono<HonoEnv>()
return c.json({ error: 'File too large' }, 400); return c.json({ error: 'File too large' }, 400);
} }
const result = await putNormalizedPdfFileServerSide(file); const arrayBuffer = await file.arrayBuffer();
const pdf = await PDFDocument.load(arrayBuffer).catch((e) => {
console.error(`PDF upload parse error: ${e.message}`);
throw new AppError('INVALID_DOCUMENT_FILE');
});
if (pdf.isEncrypted) {
throw new AppError('INVALID_DOCUMENT_FILE');
}
// Todo: (RR7) Test this.
if (!file.name.endsWith('.pdf')) {
Object.defineProperty(file, 'name', {
writable: true,
value: `${file.name}.pdf`,
});
}
const { type, data } = await putFileServerSide(file);
const result = await createDocumentData({ type, data });
return c.json(result); return c.json(result);
} catch (error) { } catch (error) {
@ -51,6 +72,19 @@ export const filesRoute = new Hono<HonoEnv>()
return c.json({ error: 'Upload failed' }, 500); return c.json({ error: 'Upload failed' }, 500);
} }
}) })
.post('/presigned-get-url', sValidator('json', ZGetPresignedGetUrlRequestSchema), async (c) => {
const { key } = await c.req.json();
try {
const { url } = await getPresignGetUrl(key || '');
return c.json({ url } satisfies TGetPresignedGetUrlResponse);
} catch (err) {
console.error(err);
throw new AppError(AppErrorCode.UNKNOWN_ERROR);
}
})
.post('/presigned-post-url', sValidator('json', ZGetPresignedPostUrlRequestSchema), async (c) => { .post('/presigned-post-url', sValidator('json', ZGetPresignedPostUrlRequestSchema), async (c) => {
const { fileName, contentType } = c.req.valid('json'); const { fileName, contentType } = c.req.valid('json');
@ -63,222 +97,4 @@ export const filesRoute = new Hono<HonoEnv>()
throw new AppError(AppErrorCode.UNKNOWN_ERROR); throw new AppError(AppErrorCode.UNKNOWN_ERROR);
} }
})
.get(
'/envelope/:envelopeId/envelopeItem/:envelopeItemId',
sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema),
async (c) => {
const { envelopeId, envelopeItemId } = c.req.valid('param');
const session = await getOptionalSession(c);
if (!session.user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
},
include: {
envelopeItems: {
where: {
id: envelopeItemId,
},
include: {
documentData: true,
},
},
},
}); });
if (!envelope) {
return c.json({ error: 'Envelope not found' }, 404);
}
const [envelopeItem] = envelope.envelopeItems;
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
const team = await getTeamById({
userId: session.user.id,
teamId: envelope.teamId,
}).catch((error) => {
console.error(error);
return null;
});
if (!team) {
return c.json(
{ error: 'User does not have access to the team that this envelope is associated with' },
403,
);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelope.status,
documentData: envelopeItem.documentData,
version: 'signed',
isDownload: false,
context: c,
});
},
)
.get(
'/envelope/:envelopeId/envelopeItem/:envelopeItemId/download/:version?',
sValidator('param', ZGetEnvelopeItemFileDownloadRequestParamsSchema),
async (c) => {
const { envelopeId, envelopeItemId, version } = c.req.valid('param');
const session = await getOptionalSession(c);
if (!session.user) {
return c.json({ error: 'Unauthorized' }, 401);
}
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
},
include: {
envelopeItems: {
where: {
id: envelopeItemId,
},
include: {
documentData: true,
},
},
},
});
if (!envelope) {
return c.json({ error: 'Envelope not found' }, 404);
}
const [envelopeItem] = envelope.envelopeItems;
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
const team = await getTeamById({
userId: session.user.id,
teamId: envelope.teamId,
}).catch((error) => {
console.error(error);
return null;
});
if (!team) {
return c.json(
{ error: 'User does not have access to the team that this envelope is associated with' },
403,
);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelope.status,
documentData: envelopeItem.documentData,
version,
isDownload: true,
context: c,
});
},
)
.get(
'/token/:token/envelopeItem/:envelopeItemId',
sValidator('param', ZGetEnvelopeItemFileTokenRequestParamsSchema),
async (c) => {
const { token, envelopeItemId } = c.req.valid('param');
const envelopeItem = await prisma.envelopeItem.findFirst({
where: {
id: envelopeItemId,
envelope: {
recipients: {
some: {
token,
},
},
},
},
include: {
envelope: true,
documentData: true,
},
});
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelopeItem.envelope.status,
documentData: envelopeItem.documentData,
version: 'signed',
isDownload: false,
context: c,
});
},
)
.get(
'/token/:token/envelopeItem/:envelopeItemId/download/:version?',
sValidator('param', ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema),
async (c) => {
const { token, envelopeItemId, version } = c.req.valid('param');
const envelopeItem = await prisma.envelopeItem.findFirst({
where: {
id: envelopeItemId,
envelope: {
recipients: {
some: {
token,
},
},
},
},
include: {
envelope: true,
documentData: true,
},
});
if (!envelopeItem) {
return c.json({ error: 'Envelope item not found' }, 404);
}
if (!envelopeItem.documentData) {
return c.json({ error: 'Document data not found' }, 404);
}
return await handleEnvelopeItemFileRequest({
title: envelopeItem.title,
status: envelopeItem.envelope.status,
documentData: envelopeItem.documentData,
version,
isDownload: true,
context: c,
});
},
);

View File

@ -24,43 +24,15 @@ export const ZGetPresignedPostUrlResponseSchema = z.object({
url: z.string().min(1), url: z.string().min(1),
}); });
export const ZGetPresignedGetUrlRequestSchema = z.object({
key: z.string().min(1),
});
export const ZGetPresignedGetUrlResponseSchema = z.object({
url: z.string().min(1),
});
export type TGetPresignedPostUrlRequest = z.infer<typeof ZGetPresignedPostUrlRequestSchema>; export type TGetPresignedPostUrlRequest = z.infer<typeof ZGetPresignedPostUrlRequestSchema>;
export type TGetPresignedPostUrlResponse = z.infer<typeof ZGetPresignedPostUrlResponseSchema>; export type TGetPresignedPostUrlResponse = z.infer<typeof ZGetPresignedPostUrlResponseSchema>;
export type TGetPresignedGetUrlRequest = z.infer<typeof ZGetPresignedGetUrlRequestSchema>;
export const ZGetEnvelopeItemFileRequestParamsSchema = z.object({ export type TGetPresignedGetUrlResponse = z.infer<typeof ZGetPresignedGetUrlResponseSchema>;
envelopeId: z.string().min(1),
envelopeItemId: z.string().min(1),
});
export type TGetEnvelopeItemFileRequestParams = z.infer<
typeof ZGetEnvelopeItemFileRequestParamsSchema
>;
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
token: z.string().min(1),
envelopeItemId: z.string().min(1),
});
export type TGetEnvelopeItemFileTokenRequestParams = z.infer<
typeof ZGetEnvelopeItemFileTokenRequestParamsSchema
>;
export const ZGetEnvelopeItemFileDownloadRequestParamsSchema = z.object({
envelopeId: z.string().min(1),
envelopeItemId: z.string().min(1),
version: z.enum(['signed', 'original']).default('signed'),
});
export type TGetEnvelopeItemFileDownloadRequestParams = z.infer<
typeof ZGetEnvelopeItemFileDownloadRequestParamsSchema
>;
export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
token: z.string().min(1),
envelopeItemId: z.string().min(1),
version: z.enum(['signed', 'original']).default('signed'),
});
export type TGetEnvelopeItemFileTokenDownloadRequestParams = z.infer<
typeof ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema
>;

View File

@ -8,7 +8,7 @@ import type { Logger } from 'pino';
import { tsRestHonoApp } from '@documenso/api/hono'; import { tsRestHonoApp } from '@documenso/api/hono';
import { auth } from '@documenso/auth/server'; import { auth } from '@documenso/auth/server';
import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app'; import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
import { jobsClient } from '@documenso/lib/jobs/client'; import { jobsClient } from '@documenso/lib/jobs/client';
import { getIpAddress } from '@documenso/lib/universal/get-ip-address'; import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
import { logger } from '@documenso/lib/utils/logger'; import { logger } from '@documenso/lib/utils/logger';
@ -89,22 +89,9 @@ app.route('/api/v1', tsRestHonoApp);
app.use('/api/jobs/*', jobsClient.getApiHandler()); app.use('/api/jobs/*', jobsClient.getApiHandler());
app.use('/api/trpc/*', reactRouterTrpcServer); app.use('/api/trpc/*', reactRouterTrpcServer);
// Unstable API server routes. Order matters for these two.
app.get(`${API_V2_URL}/openapi.json`, (c) => c.json(openApiDocument));
app.use(`${API_V2_URL}/*`, cors());
app.use(`${API_V2_URL}/*`, async (c) =>
openApiTrpcServerHandler(c, {
isBeta: false,
}),
);
// Unstable API server routes. Order matters for these two. // Unstable API server routes. Order matters for these two.
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument)); app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
app.use(`${API_V2_BETA_URL}/*`, cors()); app.use(`${API_V2_BETA_URL}/*`, cors());
app.use(`${API_V2_BETA_URL}/*`, async (c) => app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c));
openApiTrpcServerHandler(c, {
isBeta: true,
}),
);
export default app; export default app;

View File

@ -1,22 +1,15 @@
import type { Context } from 'hono'; import type { Context } from 'hono';
import { createOpenApiFetchHandler } from 'trpc-to-openapi';
import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app'; import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error'; import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
import { createTrpcContext } from '@documenso/trpc/server/context'; import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router'; import { appRouter } from '@documenso/trpc/server/router';
import { createOpenApiFetchHandler } from '@documenso/trpc/utils/openapi-fetch-handler';
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler'; import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
type OpenApiTrpcServerHandlerOptions = { export const openApiTrpcServerHandler = async (c: Context) => {
isBeta: boolean;
};
export const openApiTrpcServerHandler = async (
c: Context,
{ isBeta }: OpenApiTrpcServerHandlerOptions,
) => {
return createOpenApiFetchHandler<typeof appRouter>({ return createOpenApiFetchHandler<typeof appRouter>({
endpoint: isBeta ? API_V2_BETA_URL : API_V2_URL, endpoint: API_V2_BETA_URL,
router: appRouter, router: appRouter,
createContext: async () => createTrpcContext({ c, requestSource: 'apiV2' }), createContext: async () => createTrpcContext({ c, requestSource: 'apiV2' }),
req: c.req.raw, req: c.req.raw,

Binary file not shown.

Binary file not shown.

858
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@
"@commitlint/cli": "^17.7.1", "@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0", "@commitlint/config-conventional": "^17.7.0",
"@lingui/cli": "^5.2.0", "@lingui/cli": "^5.2.0",
"@prisma/client": "^6.18.0", "@prisma/client": "^6.8.2",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^8.40.0", "eslint": "^8.40.0",
@ -54,21 +54,11 @@
"nodemailer": "^6.10.1", "nodemailer": "^6.10.1",
"playwright": "1.52.0", "playwright": "1.52.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prisma": "^6.18.0", "prisma": "^6.8.2",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"turbo": "^1.9.3", "turbo": "^1.9.3",
"@trpc/client": "11.7.0",
"@trpc/react-query": "11.7.0",
"@trpc/server": "11.7.0",
"superjson": "^2.2.5",
"trpc-to-openapi": "2.4.0",
"zod-openapi": "^4.2.4",
"@ts-rest/core": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
"zod-prisma-types": "3.3.5",
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"name": "@documenso/root", "name": "@documenso/root",
@ -86,10 +76,10 @@
"mupdf": "^1.0.0", "mupdf": "^1.0.0",
"react": "^18", "react": "^18",
"typescript": "5.6.2", "typescript": "5.6.2",
"zod": "^3.25.76" "zod": "3.24.1"
}, },
"overrides": { "overrides": {
"zod": "^3.25.76" "zod": "3.24.1"
}, },
"trigger.dev": { "trigger.dev": {
"endpointId": "documenso-app" "endpointId": "documenso-app"

View File

@ -17,14 +17,14 @@
"dependencies": { "dependencies": {
"@documenso/lib": "*", "@documenso/lib": "*",
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@ts-rest/core": "^3.52.0", "@ts-rest/core": "^3.30.5",
"@ts-rest/open-api": "^3.52.0", "@ts-rest/open-api": "^3.33.0",
"@ts-rest/serverless": "^3.52.0", "@ts-rest/serverless": "^3.30.5",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"superjson": "^2.2.5", "superjson": "^1.13.1",
"swagger-ui-react": "^5.21.0", "swagger-ui-react": "^5.21.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
} }
} }

View File

@ -20,12 +20,12 @@ import {
getEnvelopeWhereInput, getEnvelopeWhereInput,
} from '@documenso/lib/server-only/envelope/get-envelope-by-id'; } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field'; import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
import { updateEnvelopeFields } from '@documenso/lib/server-only/field/update-envelope-fields'; import { updateDocumentFields } from '@documenso/lib/server-only/field/update-document-fields';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf'; import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { deleteEnvelopeRecipient } from '@documenso/lib/server-only/recipient/delete-envelope-recipient'; import { deleteDocumentRecipient } from '@documenso/lib/server-only/recipient/delete-document-recipient';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients'; import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { updateEnvelopeRecipients } from '@documenso/lib/server-only/recipient/update-envelope-recipients'; import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
@ -1285,7 +1285,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}; };
} }
const updatedRecipient = await updateEnvelopeRecipients({ const updatedRecipient = await updateDocumentRecipients({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
id: { id: {
@ -1336,7 +1336,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}, },
}); });
const deletedRecipient = await deleteEnvelopeRecipient({ const deletedRecipient = await deleteDocumentRecipient({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
recipientId: Number(recipientId), recipientId: Number(recipientId),
@ -1634,13 +1634,10 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}; };
} }
const { fields } = await updateEnvelopeFields({ const { fields } = await updateDocumentFields({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
id: { documentId: legacyDocumentId,
type: 'documentId',
id: legacyDocumentId,
},
fields: [ fields: [
{ {
id: Number(fieldId), id: Number(fieldId),

View File

@ -1,498 +0,0 @@
import { FieldType } from '@prisma/client';
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
export type FieldTestData = TFieldAndMeta & {
page: number;
positionX: number;
positionY: number;
width: number;
height: number;
customText: string;
signature?: string;
};
const columnWidth = 19.125;
const rowHeight = 6.7;
const alignmentGridStartX = 31;
const alignmentGridStartY = 19.02;
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
/**
* Row 1 EMAIL
*/
{
type: FieldType.EMAIL,
fieldMeta: {
fontSize: 10,
textAlign: 'left',
type: 'email',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'admin@documenso.com',
},
{
type: FieldType.EMAIL,
fieldMeta: {
textAlign: 'center',
type: 'email',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'admin@documenso.com',
},
{
type: FieldType.EMAIL,
fieldMeta: {
fontSize: 20,
textAlign: 'right',
type: 'email',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'admin@documenso.com',
},
/**
* Row 2 NAME
*/
{
type: FieldType.NAME,
fieldMeta: {
fontSize: 10,
textAlign: 'left',
type: 'name',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'John Doe',
},
{
type: FieldType.NAME,
fieldMeta: {
textAlign: 'center',
type: 'name',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'John Doe',
},
{
type: FieldType.NAME,
fieldMeta: {
fontSize: 20,
textAlign: 'right',
type: 'name',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'John Doe',
},
/**
* Row 3 DATE
*/
{
type: FieldType.DATE,
fieldMeta: {
fontSize: 10,
textAlign: 'left',
type: 'date',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
{
type: FieldType.DATE,
fieldMeta: {
textAlign: 'center',
type: 'date',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
{
type: FieldType.DATE,
fieldMeta: {
fontSize: 20,
textAlign: 'right',
type: 'date',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
/**
* Row 4 TEXT
*/
{
type: FieldType.TEXT,
fieldMeta: {
fontSize: 10,
textAlign: 'left',
type: 'text',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
textAlign: 'center',
type: 'text',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
fontSize: 20,
textAlign: 'right',
type: 'text',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
/**
* Row 5 NUMBER
*/
{
type: FieldType.NUMBER,
fieldMeta: {
fontSize: 10,
textAlign: 'left',
type: 'number',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
textAlign: 'center',
type: 'number',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
fontSize: 20,
textAlign: 'right',
type: 'number',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '123456789',
},
/**
* Row 6 Initials
*/
{
type: FieldType.INITIALS,
fieldMeta: {
fontSize: 10,
textAlign: 'left',
type: 'initials',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'JD',
},
{
type: FieldType.INITIALS,
fieldMeta: {
textAlign: 'center',
type: 'initials',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'JD',
},
{
type: FieldType.INITIALS,
fieldMeta: {
fontSize: 20,
textAlign: 'right',
type: 'initials',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'JD',
},
/**
* Row 7 Radio
*/
{
type: FieldType.RADIO,
fieldMeta: {
fontSize: 10,
direction: 'vertical',
type: 'radio',
values: [
{ id: 1, checked: true, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
],
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '0',
},
{
type: FieldType.RADIO,
fieldMeta: {
direction: 'vertical',
type: 'radio',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' },
],
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '2',
},
{
type: FieldType.RADIO,
fieldMeta: {
fontSize: 20,
direction: 'horizontal',
type: 'radio',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
],
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
},
/**
* Row 8 Checkbox
*/
{
type: FieldType.CHECKBOX,
fieldMeta: {
fontSize: 10,
direction: 'vertical',
type: 'checkbox',
values: [
{ id: 1, checked: true, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
],
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: toCheckboxCustomText([0]),
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' },
],
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: toCheckboxCustomText([1]),
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
fontSize: 20,
direction: 'horizontal',
type: 'checkbox',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
],
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
},
/**
* Row 8 Dropdown
*/
{
type: FieldType.DROPDOWN,
fieldMeta: {
fontSize: 10,
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
type: 'dropdown',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'Option 1',
},
{
type: FieldType.DROPDOWN,
fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
type: 'dropdown',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'Option 1',
},
{
type: FieldType.DROPDOWN,
fieldMeta: {
fontSize: 20,
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
type: 'dropdown',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: 'Option 1',
},
/**
* Row 9 Signature
*/
{
type: FieldType.SIGNATURE,
fieldMeta: {
fontSize: 10,
type: 'signature',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
signature: 'My Signature',
},
{
type: FieldType.SIGNATURE,
fieldMeta: {
type: 'signature',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
signature: 'My Signature',
},
{
type: FieldType.SIGNATURE,
fieldMeta: {
fontSize: 20,
type: 'signature',
},
page: 1,
height: rowHeight,
width: columnWidth,
positionX: 0,
positionY: 0,
customText: '',
signature: 'My Signature',
},
] as const;
export const formatAlignmentTestFields = ALIGNMENT_TEST_FIELDS.map((field, index) => {
const row = Math.floor(index / 3);
const column = index % 3;
return {
...field,
positionX: alignmentGridStartX + column * columnWidth,
positionY: alignmentGridStartY + row * rowHeight,
};
});

View File

@ -1,482 +0,0 @@
import { FieldType } from '@prisma/client';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
import {
CheckboxValidationRules,
numberFormatValues,
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import type { FieldTestData } from './field-alignment-pdf';
const columnWidth = 20.1;
const fullColumnWidth = 75.8;
const rowHeight = 9.8;
const rowPadding = 1.8;
const alignmentGridStartX = 11.85;
const alignmentGridStartY = 15.07;
const calculatePosition = (row: number, column: number, width: 'full' | 'column' = 'column') => {
return {
height: rowHeight,
width: width === 'full' ? fullColumnWidth : columnWidth,
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
};
};
export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
/**
* PAGE 2 Signature
*/
{
type: FieldType.SIGNATURE,
fieldMeta: {
type: 'signature',
},
page: 2,
...calculatePosition(0, 0),
customText: '',
signature: 'My Signature',
},
{
type: FieldType.SIGNATURE,
fieldMeta: {
type: 'signature',
},
page: 2,
...calculatePosition(1, 0),
customText: '',
signature: 'My Signature',
},
{
type: FieldType.SIGNATURE,
fieldMeta: {
type: 'signature',
},
page: 2,
...calculatePosition(2, 0),
customText: '',
signature: 'My Signature',
},
{
type: FieldType.SIGNATURE,
fieldMeta: {
type: 'signature',
},
page: 2,
...calculatePosition(3, 0),
customText: '',
signature: 'My Signature',
},
/**
* PAGE 3 TEXT
*/
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
},
page: 3,
...calculatePosition(0, 0, 'full'),
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
},
page: 3,
...calculatePosition(1, 0),
customText: '123456789123456789123456789123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
characterLimit: 5,
},
page: 3,
...calculatePosition(2, 0),
customText: '12345',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
placeholder: 'Demo Placeholder',
},
page: 3,
...calculatePosition(3, 0),
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
label: 'Demo Label',
},
page: 3,
...calculatePosition(3, 1),
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
text: 'Prefilled text',
},
page: 3,
...calculatePosition(3, 2),
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
required: true,
},
page: 3,
...calculatePosition(4, 0),
customText: '123456789',
},
{
type: FieldType.TEXT,
fieldMeta: {
type: 'text',
readOnly: true,
text: 'Readonly Value',
},
page: 3,
...calculatePosition(4, 1),
customText: 'Readonly Value',
},
/**
* PAGE 4 NUMBER
*/
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
},
page: 4,
...calculatePosition(0, 0, 'full'),
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
},
page: 4,
...calculatePosition(1, 0),
customText: '123456789123456789123456789123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
minValue: 0,
maxValue: 100,
},
page: 4,
...calculatePosition(2, 0),
customText: '50',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
numberFormat: numberFormatValues[0].value, // Todo: Envelopes - Check this.
value: '123,456,789.00',
},
page: 4,
...calculatePosition(2, 1),
customText: '123,456,789.00',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
placeholder: 'Demo Placeholder',
},
page: 4,
...calculatePosition(3, 0),
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
label: 'Demo Label',
},
page: 4,
...calculatePosition(3, 1),
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
value: '123',
},
page: 4,
...calculatePosition(3, 2),
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
required: true,
},
page: 4,
...calculatePosition(4, 0),
customText: '123456789',
},
{
type: FieldType.NUMBER,
fieldMeta: {
type: 'number',
readOnly: true,
},
page: 4,
...calculatePosition(4, 1),
customText: '123456789',
},
/**
* PAGE 5 RADIO
*/
{
type: FieldType.RADIO,
fieldMeta: {
direction: 'horizontal',
type: 'radio',
values: [
{ id: 1, checked: true, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 5,
...calculatePosition(0, 0, 'full'),
customText: '0',
},
{
type: FieldType.RADIO,
fieldMeta: {
direction: 'vertical',
type: 'radio',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 5,
...calculatePosition(1, 0),
customText: '2',
},
{
type: FieldType.RADIO,
fieldMeta: {
direction: 'vertical',
type: 'radio',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 5,
...calculatePosition(2, 0),
customText: '',
},
{
type: FieldType.RADIO,
fieldMeta: {
direction: 'vertical',
type: 'radio',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 5,
...calculatePosition(2, 1),
customText: '',
},
/**
* PAGE 6 CHECKBOX
*/
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'horizontal',
type: 'checkbox',
values: [
{ id: 1, checked: true, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 2, checked: false, value: 'Option 3' },
{ id: 2, checked: false, value: 'Option 4' },
],
},
page: 6,
...calculatePosition(0, 0, 'full'),
customText: toCheckboxCustomText([0]),
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: true, value: 'Option 2' },
{ id: 2, checked: true, value: 'Option 3' },
],
},
page: 6,
...calculatePosition(1, 0),
customText: toCheckboxCustomText([1]),
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
required: true,
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
],
},
page: 6,
...calculatePosition(2, 0),
customText: '',
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
readOnly: true,
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
],
},
page: 6,
...calculatePosition(2, 1),
customText: '',
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
validationRule: CheckboxValidationRules.SELECT_AT_LEAST,
validationLength: 2,
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 6,
...calculatePosition(3, 0),
customText: '',
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
validationRule: CheckboxValidationRules.SELECT_EXACTLY,
validationLength: 2,
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 6,
...calculatePosition(3, 1),
customText: '',
},
{
type: FieldType.CHECKBOX,
fieldMeta: {
direction: 'vertical',
type: 'checkbox',
validationRule: CheckboxValidationRules.SELECT_AT_MOST,
validationLength: 2,
values: [
{ id: 1, checked: false, value: 'Option 1' },
{ id: 2, checked: false, value: 'Option 2' },
{ id: 3, checked: false, value: 'Option 3' },
],
},
page: 6,
...calculatePosition(3, 2),
customText: '',
},
/**
* PAGE 7 DROPDOWN
*/
{
type: FieldType.DROPDOWN,
fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
type: 'dropdown',
},
page: 7,
...calculatePosition(0, 0, 'full'),
customText: 'Option 1',
},
{
type: FieldType.DROPDOWN,
fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
type: 'dropdown',
defaultValue: 'Option 1',
},
page: 7,
...calculatePosition(1, 0),
customText: 'Option 1',
},
{
type: FieldType.DROPDOWN,
fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
type: 'dropdown',
required: true,
},
page: 7,
...calculatePosition(2, 0),
customText: 'Option 1',
},
{
type: FieldType.DROPDOWN,
fieldMeta: {
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
type: 'dropdown',
readOnly: true,
},
page: 7,
...calculatePosition(2, 1),
customText: 'Option 1',
},
] as const;
export const formatFieldMetaTestFields = FIELD_META_TEST_FIELDS.map((field, index) => {
return {
...field,
};
});

View File

@ -1,563 +0,0 @@
import { expect, test } from '@playwright/test';
import type { Team, User } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import { pick } from 'remeda';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import {
DocumentDistributionMethod,
DocumentSigningOrder,
DocumentStatus,
DocumentVisibility,
EnvelopeType,
FieldType,
FolderType,
RecipientRole,
} from '@documenso/prisma/client';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TCreateEnvelopeItemsRequest } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type {
TCreateEnvelopePayload,
TCreateEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
import { formatAlignmentTestFields } from '../../../constants/field-alignment-pdf';
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
test.describe.configure({
mode: 'parallel',
});
test.describe('API V2 Envelopes', () => {
let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string;
test.beforeEach(async () => {
({ user: userA, team: teamA } = await seedUser());
({ token: tokenA } = await createApiToken({
userId: userA.id,
teamId: teamA.id,
tokenName: 'userA',
expiresIn: null,
}));
({ user: userB, team: teamB } = await seedUser());
({ token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
}));
});
test.describe('Envelope create endpoint', () => {
test('should fail on invalid form', async ({ request }) => {
const payload = {
type: 'Invalid Type',
title: 'Test Envelope',
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${tokenB}` },
multipart: formData,
});
expect(res.ok()).toBeFalsy();
expect(res.status()).toBe(400);
});
test('should create envelope with single file', async ({ request }) => {
const payload = {
type: EnvelopeType.TEMPLATE,
title: 'Test Envelope',
} satisfies TCreateEnvelopePayload;
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const files = [
{
name: 'field-font-alignment.pdf',
data: fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
),
},
];
for (const file of files) {
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
}
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${tokenB}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUnique({
where: {
id: response.id,
},
include: {
envelopeItems: true,
},
});
expect(envelope).toBeDefined();
expect(envelope?.title).toBe('Test Envelope');
expect(envelope?.type).toBe(EnvelopeType.TEMPLATE);
expect(envelope?.status).toBe(DocumentStatus.DRAFT);
expect(envelope?.envelopeItems.length).toBe(1);
expect(envelope?.envelopeItems[0].title).toBe('field-font-alignment.pdf');
expect(envelope?.envelopeItems[0].documentDataId).toBeDefined();
});
test('should create envelope with multiple file', async ({ request }) => {
const folder = await prisma.folder.create({
data: {
name: 'Test Folder',
teamId: teamA.id,
userId: userA.id,
type: FolderType.DOCUMENT,
},
});
const payload = {
title: 'Envelope Title',
type: EnvelopeType.DOCUMENT,
externalId: 'externalId',
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
globalAccessAuth: ['ACCOUNT'],
formValues: {
hello: 'world',
},
folderId: folder.id,
recipients: [
{
email: userA.email,
name: 'Name',
role: RecipientRole.SIGNER,
accessAuth: ['TWO_FACTOR_AUTH'],
signingOrder: 1,
fields: [
{
type: FieldType.SIGNATURE,
identifier: 'field-font-alignment.pdf',
page: 1,
positionX: 0,
positionY: 0,
width: 0,
height: 0,
},
{
type: FieldType.SIGNATURE,
identifier: 0,
page: 1,
positionX: 0,
positionY: 0,
width: 0,
height: 0,
},
],
},
],
meta: {
subject: 'Subject',
message: 'Message',
timezone: 'Europe/Berlin',
dateFormat: 'dd.MM.yyyy',
distributionMethod: DocumentDistributionMethod.NONE,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
allowDictateNextSigner: true,
redirectUrl: 'https://documenso.com',
language: 'de',
typedSignatureEnabled: true,
uploadSignatureEnabled: false,
drawSignatureEnabled: false,
emailReplyTo: userA.email,
emailSettings: {
recipientSigningRequest: false,
recipientRemoved: false,
recipientSigned: false,
documentPending: false,
documentCompleted: false,
documentDeleted: false,
ownerDocumentCompleted: true,
},
},
attachments: [
{
label: 'Test Attachment',
data: 'https://documenso.com',
type: 'link',
},
],
} satisfies TCreateEnvelopePayload;
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const files = [
{
name: 'field-meta.pdf',
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/field-meta.pdf')),
},
{
name: 'field-font-alignment.pdf',
data: fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
),
},
];
for (const file of files) {
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
}
// Should error since folder is not owned by the user.
const invalidRes = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${tokenB}` },
multipart: formData,
});
expect(invalidRes.ok()).toBeFalsy();
const res = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${tokenA}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const response = (await res.json()) as TCreateEnvelopeResponse;
const envelope = await prisma.envelope.findUniqueOrThrow({
where: {
id: response.id,
},
include: {
documentMeta: true,
envelopeItems: true,
recipients: true,
fields: true,
envelopeAttachments: true,
},
});
console.log(userB.email);
expect(envelope.envelopeItems.length).toBe(2);
expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');
expect(envelope.title).toBe(payload.title);
expect(envelope.type).toBe(payload.type);
expect(envelope.externalId).toBe(payload.externalId);
expect(envelope.visibility).toBe(payload.visibility);
expect(envelope.authOptions).toEqual({
globalAccessAuth: payload.globalAccessAuth,
globalActionAuth: [],
});
expect(envelope.formValues).toEqual(payload.formValues);
expect(envelope.folderId).toBe(payload.folderId);
expect(envelope.documentMeta.subject).toBe(payload.meta.subject);
expect(envelope.documentMeta.message).toBe(payload.meta.message);
expect(envelope.documentMeta.timezone).toBe(payload.meta.timezone);
expect(envelope.documentMeta.dateFormat).toBe(payload.meta.dateFormat);
expect(envelope.documentMeta.distributionMethod).toBe(payload.meta.distributionMethod);
expect(envelope.documentMeta.signingOrder).toBe(payload.meta.signingOrder);
expect(envelope.documentMeta.allowDictateNextSigner).toBe(
payload.meta.allowDictateNextSigner,
);
expect(envelope.documentMeta.redirectUrl).toBe(payload.meta.redirectUrl);
expect(envelope.documentMeta.language).toBe(payload.meta.language);
expect(envelope.documentMeta.typedSignatureEnabled).toBe(payload.meta.typedSignatureEnabled);
expect(envelope.documentMeta.uploadSignatureEnabled).toBe(
payload.meta.uploadSignatureEnabled,
);
expect(envelope.documentMeta.drawSignatureEnabled).toBe(payload.meta.drawSignatureEnabled);
expect(envelope.documentMeta.emailReplyTo).toBe(payload.meta.emailReplyTo);
expect(envelope.documentMeta.emailSettings).toEqual(payload.meta.emailSettings);
expect([
{
label: envelope.envelopeAttachments[0].label,
data: envelope.envelopeAttachments[0].data,
type: envelope.envelopeAttachments[0].type,
},
]).toEqual(payload.attachments);
const field = envelope.fields[0];
const recipient = envelope.recipients[0];
expect({
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
accessAuth: recipient.authOptions?.accessAuth,
}).toEqual(
pick(payload.recipients[0], ['email', 'name', 'role', 'signingOrder', 'accessAuth']),
);
expect({
type: field.type,
page: field.page,
positionX: field.positionX.toNumber(),
positionY: field.positionY.toNumber(),
width: field.width.toNumber(),
height: field.height.toNumber(),
}).toEqual(
pick(payload.recipients[0].fields[0], [
'type',
'page',
'positionX',
'positionY',
'width',
'height',
]),
);
// Expect string based ID to work.
expect(field.envelopeItemId).toBe(
envelope.envelopeItems.find((item) => item.title === 'field-font-alignment.pdf')?.id,
);
// Expect index based ID to work.
expect(envelope.fields[1].envelopeItemId).toBe(
envelope.envelopeItems.find((item) => item.title === 'field-meta.pdf')?.id,
);
});
});
/**
* Creates envelopes with the two field test PDFs.
*/
test('Envelope full test', async ({ request }) => {
// Step 1: Create initial envelope with Prisma (with first envelope item)
const alignmentPdf = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
);
const fieldMetaPdf = fs.readFileSync(
path.join(__dirname, '../../../../../assets/field-meta.pdf'),
);
const formData = new FormData();
formData.append(
'payload',
JSON.stringify({
type: EnvelopeType.DOCUMENT,
title: 'Envelope Full Field Test',
} satisfies TCreateEnvelopePayload),
);
// Only add one file for now.
formData.append(
'files',
new File([alignmentPdf], 'field-font-alignment.pdf', { type: 'application/pdf' }),
);
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
headers: { Authorization: `Bearer ${tokenA}` },
multipart: formData,
});
expect(createEnvelopeRequest.ok()).toBeTruthy();
expect(createEnvelopeRequest.status()).toBe(200);
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
const getEnvelopeRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
const createdEnvelope: TGetEnvelopeResponse = await getEnvelopeRequest.json();
// Might as well testing access control here as well.
const unauthRequest = await request.get(`${baseUrl}/envelope/${createdEnvelopeId}`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(unauthRequest.ok()).toBeFalsy();
expect(unauthRequest.status()).toBe(404);
// Step 2: Create second envelope item via API
// Todo: Envelopes - Use API Route
const fieldMetaDocumentData = await prisma.documentData.create({
data: {
type: 'BYTES_64',
data: fieldMetaPdf.toString('base64'),
initialData: fieldMetaPdf.toString('base64'),
},
});
const createEnvelopeItemsRequest: TCreateEnvelopeItemsRequest = {
envelopeId: createdEnvelope.id,
data: [
{
title: 'Field Meta Test',
documentDataId: fieldMetaDocumentData.id,
},
],
};
const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: createEnvelopeItemsRequest,
});
expect(createItemsRes.ok()).toBeTruthy();
expect(createItemsRes.status()).toBe(200);
// Step 3: Update envelope via API
const updateEnvelopeRequest: TUpdateEnvelopeRequest = {
envelopeId: createdEnvelope.id,
data: {
title: 'Envelope Full Field Test',
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
},
};
const updateRes = await request.post(`${baseUrl}/envelope/update`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: updateEnvelopeRequest,
});
expect(updateRes.ok()).toBeTruthy();
expect(updateRes.status()).toBe(200);
// Step 4: Create recipient via API
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
envelopeId: createdEnvelope.id,
data: [
{
email: userA.email,
name: userA.name || '',
role: RecipientRole.SIGNER,
accessAuth: [],
actionAuth: [],
},
],
};
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: createRecipientsRequest,
});
expect(createRecipientsRes.ok()).toBeTruthy();
expect(createRecipientsRes.status()).toBe(200);
// Step 5: Get envelope to retrieve recipients and envelope items
const getRes = await request.get(`${baseUrl}/envelope/${createdEnvelope.id}`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(getRes.ok()).toBeTruthy();
expect(getRes.status()).toBe(200);
const envelopeResponse = (await getRes.json()) as TGetEnvelopeResponse;
const recipientId = envelopeResponse.recipients[0].id;
const alignmentItem = envelopeResponse.envelopeItems.find(
(item: { order: number }) => item.order === 1,
);
const fieldMetaItem = envelopeResponse.envelopeItems.find(
(item: { order: number }) => item.order === 2,
);
expect(recipientId).toBeDefined();
expect(alignmentItem).toBeDefined();
expect(fieldMetaItem).toBeDefined();
if (!alignmentItem || !fieldMetaItem) {
throw new Error('Envelope items not found');
}
// Step 6: Create fields for first PDF (alignment fields)
const alignmentFieldsRequest = {
envelopeId: createdEnvelope.id,
data: formatAlignmentTestFields.map((field) => ({
recipientId,
envelopeItemId: alignmentItem.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
fieldMeta: field.fieldMeta,
})),
};
const createAlignmentFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: alignmentFieldsRequest,
});
expect(createAlignmentFieldsRes.ok()).toBeTruthy();
expect(createAlignmentFieldsRes.status()).toBe(200);
// Step 7: Create fields for second PDF (field-meta fields)
const fieldMetaFieldsRequest = {
envelopeId: createdEnvelope.id,
data: FIELD_META_TEST_FIELDS.map((field) => ({
recipientId,
envelopeItemId: fieldMetaItem.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
fieldMeta: field.fieldMeta,
})),
};
const createFieldMetaFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
headers: { Authorization: `Bearer ${tokenA}` },
data: fieldMetaFieldsRequest,
});
expect(createFieldMetaFieldsRes.ok()).toBeTruthy();
expect(createFieldMetaFieldsRes.status()).toBe(200);
// Step 8: Verify final envelope structure
const finalGetRes = await request.get(`${baseUrl}/envelope/${createdEnvelope.id}`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(finalGetRes.ok()).toBeTruthy();
const finalEnvelope = (await finalGetRes.json()) as TGetEnvelopeResponse;
// Verify structure
expect(finalEnvelope.envelopeItems.length).toBe(2);
expect(finalEnvelope.recipients.length).toBe(1);
expect(finalEnvelope.fields.length).toBe(
formatAlignmentTestFields.length + FIELD_META_TEST_FIELDS.length,
);
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
});
});

View File

@ -1,282 +0,0 @@
// sort-imports-ignore
// ---- PATCH pdfjs-dist's canvas require BEFORE importing it ----
import Module from 'module';
import { Canvas, Image } from 'skia-canvas';
// Intercept require('canvas') and return skia-canvas equivalents
const originalRequire = Module.prototype.require;
Module.prototype.require = function (path: string) {
if (path === 'canvas') {
return {
createCanvas: (width: number, height: number) => new Canvas(width, height),
Image, // needed by pdfjs-dist
};
}
// eslint-disable-next-line prefer-rest-params, @typescript-eslint/consistent-type-assertions
return originalRequire.apply(this, arguments as unknown as [string]);
};
import pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import type { TestInfo } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel', timeout: 60000 });
test('field placement visual regression', async ({ page }, testInfo) => {
const { user, team } = await seedUser();
const envelope = await seedAlignmentTestDocument({
userId: user.id,
teamId: team.id,
recipientName: user.name || '',
recipientEmail: user.email,
insertFields: true,
status: DocumentStatus.PENDING,
});
const token = envelope.recipients[0].token;
const signUrl = `/sign/${token}`;
await apiSignin({
page,
email: user.email,
redirectPath: signUrl,
});
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`${signUrl}/complete`);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: {
id: envelope.id,
},
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({
timeout: 10000,
});
const completedDocument = await prisma.envelope.findFirstOrThrow({
where: {
id: envelope.id,
},
include: {
envelopeItems: {
orderBy: {
order: 'asc',
},
include: {
documentData: true,
},
},
},
});
const storedImages = fs.readdirSync(path.join(__dirname, '../../visual-regression'));
await Promise.all(
completedDocument.envelopeItems.map(async (item) => {
const pdfData = await getFile(item.documentData);
const loadedImages = storedImages
.filter((image) => image.includes(item.title))
.map((image) => fs.readFileSync(path.join(__dirname, '../../visual-regression', image)));
await compareSignedPdfWithImages({
id: item.title.replaceAll(' ', '-').toLowerCase(),
pdfData,
images: loadedImages,
testInfo,
});
}),
);
});
/**
* Used to download the envelope images when updating the visual regression test.
*
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
*/
test.skip('download envelope images', async ({ page }) => {
const { user, team } = await seedUser();
const envelope = await seedAlignmentTestDocument({
userId: user.id,
teamId: team.id,
recipientName: user.name || '',
recipientEmail: user.email,
insertFields: true,
status: DocumentStatus.PENDING,
});
const token = envelope.recipients[0].token;
const signUrl = `/sign/${token}`;
await apiSignin({
page,
email: user.email,
redirectPath: signUrl,
});
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`${signUrl}/complete`);
await expect(async () => {
const { status } = await prisma.envelope.findFirstOrThrow({
where: {
id: envelope.id,
},
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass({
timeout: 10000,
});
const completedDocument = await prisma.envelope.findFirstOrThrow({
where: {
id: envelope.id,
},
include: {
envelopeItems: {
orderBy: {
order: 'asc',
},
include: {
documentData: true,
},
},
},
});
await Promise.all(
completedDocument.envelopeItems.map(async (item) => {
const pdfData = await getFile(item.documentData);
const pdfImages = await renderPdfToImage(pdfData);
for (const [index, { image }] of pdfImages.entries()) {
fs.writeFileSync(
path.join(__dirname, '../../visual-regression', `${item.title}-${index}.png`),
new Uint8Array(image),
);
}
}),
);
});
async function renderPdfToImage(pdfBytes: Uint8Array) {
const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
const pdf = await loadingTask.promise;
// Increase for higher resolution
const scale = 4;
return await Promise.all(
Array.from({ length: pdf.numPages }, async (_, index) => {
const page = await pdf.getPage(index + 1);
const viewport = page.getViewport({ scale });
const virtualCanvas = new Canvas(viewport.width, viewport.height);
const context = virtualCanvas.getContext('2d');
context.imageSmoothingEnabled = false;
// @ts-expect-error skia-canvas context satisfies runtime requirements for pdfjs
await page.render({ canvasContext: context, viewport }).promise;
return {
image: await virtualCanvas.toBuffer('png'),
// Rounded down because the certificate page somehow gives dimensions with decimals
width: Math.floor(viewport.width),
height: Math.floor(viewport.height),
};
}),
);
}
type CompareSignedPdfWithImagesOptions = {
id: string;
pdfData: Uint8Array;
images: Buffer[];
testInfo: TestInfo;
};
const compareSignedPdfWithImages = async ({
id,
pdfData,
images,
testInfo,
}: CompareSignedPdfWithImagesOptions) => {
const renderedImages = await renderPdfToImage(pdfData);
const blankCertificateFile = fs.readFileSync(
path.join(__dirname, '../../visual-regression/blank-certificate.png'),
);
const blankCertificateImage = PNG.sync.read(blankCertificateFile).data;
for (const [index, { image, width, height }] of renderedImages.entries()) {
const isCertificate = index === renderedImages.length - 1;
const diff = new PNG({ width, height });
const storedImage = PNG.sync.read(images[index]).data;
const newImage = PNG.sync.read(image).data;
const oldImage = isCertificate ? blankCertificateImage : storedImage;
const comparison = pixelMatch(
new Uint8Array(oldImage),
new Uint8Array(newImage),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
diff.data as unknown as Uint8Array,
width,
height,
{
threshold: 0.25,
// includeAA: true, // This allows stricter testing.
},
);
console.log(`${id}-${index}: ${comparison}`);
const diffFilePath = path.join(testInfo.outputPath(), `${id}-${index}-diff.png`);
const oldFilePath = path.join(testInfo.outputPath(), `${id}-${index}-old.png`);
const newFilePath = path.join(testInfo.outputPath(), `${id}-${index}-new.png`);
fs.writeFileSync(diffFilePath, new Uint8Array(PNG.sync.write(diff)));
fs.writeFileSync(oldFilePath, new Uint8Array(images[index]));
fs.writeFileSync(newFilePath, new Uint8Array(image));
if (isCertificate) {
// Expect the certificate to NOT be blank. Since the storedImage is blank.
expect.soft(comparison).toBeGreaterThan(20000);
} else {
expect.soft(comparison).toEqual(0);
}
}
};

View File

@ -15,10 +15,7 @@
"@documenso/lib": "*", "@documenso/lib": "*",
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@playwright/test": "1.52.0", "@playwright/test": "1.52.0",
"@types/node": "^20", "@types/node": "^20"
"@types/pngjs": "^6.0.5",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0"
}, },
"dependencies": { "dependencies": {
"start-server-and-test": "^2.0.12" "start-server-and-test": "^2.0.12"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@ -20,6 +20,6 @@
"luxon": "^3.5.0", "luxon": "^3.5.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
} }
} }

View File

@ -19,6 +19,6 @@
"micro": "^10.0.1", "micro": "^10.0.1",
"react": "^18", "react": "^18",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
} }
} }

View File

@ -165,7 +165,10 @@ export const useEditorFields = ({
const index = localFields.findIndex((field) => field.formId === formId); const index = localFields.findIndex((field) => field.formId === formId);
if (index !== -1) { if (index !== -1) {
form.setValue(`fields.${index}.id`, id); update(index, {
...localFields[index],
id,
});
} }
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import Konva from 'konva'; import Konva from 'konva';
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api'; import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
@ -25,8 +25,6 @@ export function usePageRenderer(renderFunction: RenderFunction) {
const stage = useRef<Konva.Stage | null>(null); const stage = useRef<Konva.Stage | null>(null);
const pageLayer = useRef<Konva.Layer | null>(null); const pageLayer = useRef<Konva.Layer | null>(null);
const [renderError, setRenderError] = useState<boolean>(false);
/** /**
* The raw viewport with no scaling. Basically the actual PDF size. * The raw viewport with no scaling. Basically the actual PDF size.
*/ */
@ -124,7 +122,5 @@ export function usePageRenderer(renderFunction: RenderFunction) {
unscaledViewport, unscaledViewport,
scaledViewport, scaledViewport,
pageContext, pageContext,
renderError,
setRenderError,
}; };
} }

View File

@ -215,6 +215,7 @@ export const EnvelopeEditorProvider = ({
} = useEnvelopeAutosave(async (envelopeUpdates: UpdateEnvelopePayload) => { } = useEnvelopeAutosave(async (envelopeUpdates: UpdateEnvelopePayload) => {
await envelopeUpdateMutationQuery.mutateAsync({ await envelopeUpdateMutationQuery.mutateAsync({
envelopeId: envelope.id, envelopeId: envelope.id,
envelopeType: envelope.type,
data: envelopeUpdates.data, data: envelopeUpdates.data,
meta: envelopeUpdates.meta, meta: envelopeUpdates.meta,
}); });

View File

@ -27,9 +27,6 @@ type EnvelopeRenderProviderValue = {
setCurrentEnvelopeItem: (envelopeItemId: string) => void; setCurrentEnvelopeItem: (envelopeItemId: string) => void;
fields: TEnvelope['fields']; fields: TEnvelope['fields'];
getRecipientColorKey: (recipientId: number) => TRecipientColor; getRecipientColorKey: (recipientId: number) => TRecipientColor;
renderError: boolean;
setRenderError: (renderError: boolean) => void;
}; };
interface EnvelopeRenderProviderProps { interface EnvelopeRenderProviderProps {
@ -77,8 +74,6 @@ export const EnvelopeRenderProvider = ({
const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null); const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null);
const [renderError, setRenderError] = useState<boolean>(false);
const envelopeItems = useMemo( const envelopeItems = useMemo(
() => envelope.envelopeItems.sort((a, b) => a.order - b.order), () => envelope.envelopeItems.sort((a, b) => a.order - b.order),
[envelope.envelopeItems], [envelope.envelopeItems],
@ -169,8 +164,6 @@ export const EnvelopeRenderProvider = ({
setCurrentEnvelopeItem, setCurrentEnvelopeItem,
fields: fields ?? [], fields: fields ?? [],
getRecipientColorKey, getRecipientColorKey,
renderError,
setRenderError,
}} }}
> >
{children} {children}

View File

@ -12,7 +12,6 @@ export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true'; export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const API_V2_BETA_URL = '/api/v2-beta'; export const API_V2_BETA_URL = '/api/v2-beta';
export const API_V2_URL = '/api/v2';
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com'; export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';

View File

@ -55,7 +55,7 @@
"skia-canvas": "^3.0.8", "skia-canvas": "^3.0.8",
"stripe": "^12.7.0", "stripe": "^12.7.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/browser-chromium": "1.52.0", "@playwright/browser-chromium": "1.52.0",

View File

@ -20,12 +20,7 @@ import { validateCheckboxLength } from '../../advanced-fields-validation/validat
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobs } from '../../jobs/client'; import { jobs } from '../../jobs/client';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { import { ZCheckboxFieldMeta, ZDropdownFieldMeta, ZRadioFieldMeta } from '../../types/field-meta';
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZFieldAndMetaSchema,
ZRadioFieldMeta,
} from '../../types/field-meta';
import { import {
ZWebhookDocumentSchema, ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload, mapEnvelopeToWebhookDocumentPayload,
@ -179,20 +174,9 @@ export const sendDocument = async ({
const fieldsToAutoInsert: { fieldId: number; customText: string }[] = []; const fieldsToAutoInsert: { fieldId: number; customText: string }[] = [];
// Validate and autoinsert fields for V2 envelopes. // Auto insert radio and checkboxes that have default values.
if (envelope.internalVersion === 2) { if (envelope.internalVersion === 2) {
for (const unknownField of envelope.fields) { for (const field of envelope.fields) {
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
if (parsedField.error) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
});
}
const field = parsedField.data;
const fieldId = unknownField.id;
if (field.type === FieldType.RADIO) { if (field.type === FieldType.RADIO) {
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta); const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
@ -200,7 +184,7 @@ export const sendDocument = async ({
if (checkedItemIndex !== -1) { if (checkedItemIndex !== -1) {
fieldsToAutoInsert.push({ fieldsToAutoInsert.push({
fieldId, fieldId: field.id,
customText: toRadioCustomText(checkedItemIndex), customText: toRadioCustomText(checkedItemIndex),
}); });
} }
@ -211,7 +195,7 @@ export const sendDocument = async ({
if (defaultValue && values.some((value) => value.value === defaultValue)) { if (defaultValue && values.some((value) => value.value === defaultValue)) {
fieldsToAutoInsert.push({ fieldsToAutoInsert.push({
fieldId, fieldId: field.id,
customText: defaultValue, customText: defaultValue,
}); });
} }
@ -250,9 +234,9 @@ export const sendDocument = async ({
); );
} }
if (isValid && checkedIndices.length > 0) { if (isValid) {
fieldsToAutoInsert.push({ fieldsToAutoInsert.push({
fieldId, fieldId: field.id,
customText: toCheckboxCustomText(checkedIndices), customText: toCheckboxCustomText(checkedIndices),
}); });
} }

View File

@ -16,16 +16,11 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
TDocumentAccessAuthTypes,
TDocumentActionAuthTypes,
TRecipientAccessAuthTypes,
TRecipientActionAuthTypes,
} from '../../types/document-auth';
import type { TDocumentFormValues } from '../../types/document-form-values'; import type { TDocumentFormValues } from '../../types/document-form-values';
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment'; import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
import type { TFieldAndMeta } from '../../types/field-meta';
import { import {
ZWebhookDocumentSchema, ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload, mapEnvelopeToWebhookDocumentPayload,
@ -39,25 +34,6 @@ import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-
import { getTeamSettings } from '../team/get-team-settings'; import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
type CreateEnvelopeRecipientFieldOptions = TFieldAndMeta & {
documentDataId: string;
page: number;
positionX: number;
positionY: number;
width: number;
height: number;
};
type CreateEnvelopeRecipientOptions = {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
fields?: CreateEnvelopeRecipientFieldOptions[];
};
export type CreateEnvelopeOptions = { export type CreateEnvelopeOptions = {
userId: number; userId: number;
teamId: number; teamId: number;
@ -70,6 +46,7 @@ export type CreateEnvelopeOptions = {
envelopeItems: { title?: string; documentDataId: string; order?: number }[]; envelopeItems: { title?: string; documentDataId: string; order?: number }[];
formValues?: TDocumentFormValues; formValues?: TDocumentFormValues;
timezone?: string;
userTimezone?: string; userTimezone?: string;
templateType?: TemplateType; templateType?: TemplateType;
@ -79,7 +56,7 @@ export type CreateEnvelopeOptions = {
visibility?: DocumentVisibility; visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes[]; globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[];
recipients?: CreateEnvelopeRecipientOptions[]; recipients?: TCreateEnvelopeRequest['recipients'];
folderId?: string; folderId?: string;
}; };
attachments?: Array<{ attachments?: Array<{
@ -106,6 +83,7 @@ export const createEnvelope = async ({
title, title,
externalId, externalId,
formValues, formValues,
timezone,
userTimezone, userTimezone,
folderId, folderId,
templateType, templateType,
@ -164,7 +142,6 @@ export const createEnvelope = async ({
let envelopeItems: { title?: string; documentDataId: string; order?: number }[] = let envelopeItems: { title?: string; documentDataId: string; order?: number }[] =
data.envelopeItems; data.envelopeItems;
// Todo: Envelopes - Remove
if (normalizePdf) { if (normalizePdf) {
envelopeItems = await Promise.all( envelopeItems = await Promise.all(
data.envelopeItems.map(async (item) => { data.envelopeItems.map(async (item) => {
@ -242,7 +219,7 @@ export const createEnvelope = async ({
// userTimezone is last because it's always passed in regardless of the organisation/team settings // userTimezone is last because it's always passed in regardless of the organisation/team settings
// for uploads from the frontend // for uploads from the frontend
const timezoneToUse = meta?.timezone || settings.documentTimezone || userTimezone; const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
const documentMeta = await prisma.documentMeta.create({ const documentMeta = await prisma.documentMeta.create({
data: extractDerivedDocumentMeta(settings, { data: extractDerivedDocumentMeta(settings, {

View File

@ -26,9 +26,9 @@ export interface CreateEnvelopeFieldsOptions {
envelopeItemId?: string; envelopeItemId?: string;
recipientId: number; recipientId: number;
page: number; pageNumber: number;
positionX: number; pageX: number;
positionY: number; pageY: number;
width: number; width: number;
height: number; height: number;
})[]; })[];
@ -122,9 +122,9 @@ export const createEnvelopeFields = async ({
const newlyCreatedFields = await tx.field.createManyAndReturn({ const newlyCreatedFields = await tx.field.createManyAndReturn({
data: validatedFields.map((field) => ({ data: validatedFields.map((field) => ({
type: field.type, type: field.type,
page: field.page, page: field.pageNumber,
positionX: field.positionX, positionX: field.pageX,
positionY: field.positionY, positionY: field.pageY,
width: field.width, width: field.width,
height: field.height, height: field.height,
customText: '', customText: '',

View File

@ -11,7 +11,7 @@ export type GetFieldByIdOptions = {
userId: number; userId: number;
teamId: number; teamId: number;
fieldId: number; fieldId: number;
envelopeType?: EnvelopeType; envelopeType: EnvelopeType;
}; };
export const getFieldById = async ({ export const getFieldById = async ({
@ -41,7 +41,7 @@ export const getFieldById = async ({
type: 'envelopeId', type: 'envelopeId',
id: field.envelopeId, id: field.envelopeId,
}, },
type: envelopeType ?? null, type: envelopeType,
userId, userId,
teamId, teamId,
}); });

View File

@ -158,7 +158,7 @@ export const setFieldsForDocument = async ({
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta); const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
const errors = validateNumberField( const errors = validateNumberField(
String(numberFieldParsedMeta.value || ''), String(numberFieldParsedMeta.value),
numberFieldParsedMeta, numberFieldParsedMeta,
false, false,
); );

View File

@ -10,21 +10,18 @@ import {
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { type EnvelopeIdOptions } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields'; import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientFieldsBeModified } from '../../utils/recipients'; import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateEnvelopeFieldsOptions { export interface UpdateDocumentFieldsOptions {
userId: number; userId: number;
teamId: number; teamId: number;
id: EnvelopeIdOptions; documentId: number;
type?: EnvelopeType | null; // Only used to enforce the type.
fields: { fields: {
id: number; id: number;
type?: FieldType; type?: FieldType;
pageNumber?: number; pageNumber?: number;
envelopeItemId?: string;
pageX?: number; pageX?: number;
pageY?: number; pageY?: number;
width?: number; width?: number;
@ -34,17 +31,19 @@ export interface UpdateEnvelopeFieldsOptions {
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const updateEnvelopeFields = async ({ export const updateDocumentFields = async ({
userId, userId,
teamId, teamId,
id, documentId,
type = null,
fields, fields,
requestMetadata, requestMetadata,
}: UpdateEnvelopeFieldsOptions) => { }: UpdateDocumentFieldsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id, id: {
type, type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId, userId,
teamId, teamId,
}); });
@ -54,19 +53,18 @@ export const updateEnvelopeFields = async ({
include: { include: {
recipients: true, recipients: true,
fields: true, fields: true,
envelopeItems: true,
}, },
}); });
if (!envelope) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found', message: 'Document not found',
}); });
} }
if (envelope.completedAt) { if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope already complete', message: 'Document already complete',
}); });
} }
@ -98,29 +96,6 @@ export const updateEnvelopeFields = async ({
}); });
} }
const fieldType = field.type || originalField.type;
const fieldMetaType = field.fieldMeta?.type || originalField.fieldMeta?.type;
// Not going to mess with V1 envelopes.
if (
envelope.internalVersion === 2 &&
fieldMetaType &&
fieldMetaType.toLowerCase() !== fieldType.toLowerCase()
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Field meta type does not match the field type',
});
}
if (
field.envelopeItemId &&
!envelope.envelopeItems.some((item) => item.id === field.envelopeItemId)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item not found',
});
}
return { return {
originalField, originalField,
updateData: field, updateData: field,
@ -143,14 +118,12 @@ export const updateEnvelopeFields = async ({
width: updateData.width, width: updateData.width,
height: updateData.height, height: updateData.height,
fieldMeta: updateData.fieldMeta, fieldMeta: updateData.fieldMeta,
envelopeItemId: updateData.envelopeItemId,
}, },
}); });
// Handle field updated audit log.
if (envelope.type === EnvelopeType.DOCUMENT) {
const changes = diffFieldChanges(originalField, updatedField); const changes = diffFieldChanges(originalField, updatedField);
// Handle field updated audit log.
if (changes.length > 0) { if (changes.length > 0) {
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({
@ -167,7 +140,6 @@ export const updateEnvelopeFields = async ({
}), }),
}); });
} }
}
return updatedField; return updatedField;
}), }),

View File

@ -0,0 +1,116 @@
import { EnvelopeType, type FieldType } from '@prisma/client';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateTemplateFieldsOptions {
userId: number;
teamId: number;
templateId: number;
fields: {
id: number;
type?: FieldType;
pageNumber?: number;
pageX?: number;
pageY?: number;
width?: number;
height?: number;
fieldMeta?: TFieldMetaSchema;
}[];
}
export const updateTemplateFields = async ({
userId,
teamId,
templateId,
fields,
}: UpdateTemplateFieldsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
const fieldsToUpdate = fields.map((field) => {
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
if (!originalField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Field with id ${field.id} not found`,
});
}
const recipient = envelope.recipients.find(
(recipient) => recipient.id === originalField.recipientId,
);
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient attached to field ${field.id} not found`,
});
}
// Check whether the recipient associated with the field can be modified.
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Cannot modify a field where the recipient has already interacted with the document',
});
}
return {
updateData: field,
};
});
const updatedFields = await prisma.$transaction(async (tx) => {
return await Promise.all(
fieldsToUpdate.map(async ({ updateData }) => {
const updatedField = await tx.field.update({
where: {
id: updateData.id,
},
data: {
type: updateData.type,
page: updateData.pageNumber,
positionX: updateData.pageX,
positionY: updateData.pageY,
width: updateData.width,
height: updateData.height,
fieldMeta: updateData.fieldMeta,
},
});
return updatedField;
}),
);
});
return {
fields: updatedFields.map((field) => mapFieldToLegacyField(field, envelope)),
};
};

View File

@ -1,22 +1,13 @@
import { PDFDocument } from '@cantoo/pdf-lib'; import { PDFDocument } from '@cantoo/pdf-lib';
import { AppError } from '../../errors/app-error';
import { flattenAnnotations } from './flatten-annotations'; import { flattenAnnotations } from './flatten-annotations';
import { flattenForm, removeOptionalContentGroups } from './flatten-form'; import { flattenForm, removeOptionalContentGroups } from './flatten-form';
export const normalizePdf = async (pdf: Buffer) => { export const normalizePdf = async (pdf: Buffer) => {
const pdfDoc = await PDFDocument.load(pdf).catch((e) => { const pdfDoc = await PDFDocument.load(pdf).catch(() => null);
console.error(`PDF normalization error: ${e.message}`);
throw new AppError('INVALID_DOCUMENT_FILE', { if (!pdfDoc) {
message: 'The document is not a valid PDF', return pdf;
});
});
if (pdfDoc.isEncrypted) {
throw new AppError('INVALID_DOCUMENT_FILE', {
message: 'The document is encrypted',
});
} }
removeOptionalContentGroups(pdfDoc); removeOptionalContentGroups(pdfDoc);

View File

@ -15,7 +15,7 @@ import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapRecipientToLegacyRecipient } from '../../utils/recipients'; import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface CreateEnvelopeRecipientsOptions { export interface CreateDocumentRecipientsOptions {
userId: number; userId: number;
teamId: number; teamId: number;
id: EnvelopeIdOptions; id: EnvelopeIdOptions;
@ -30,16 +30,16 @@ export interface CreateEnvelopeRecipientsOptions {
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const createEnvelopeRecipients = async ({ export const createDocumentRecipients = async ({
userId, userId,
teamId, teamId,
id, id,
recipients: recipientsToCreate, recipients: recipientsToCreate,
requestMetadata, requestMetadata,
}: CreateEnvelopeRecipientsOptions) => { }: CreateDocumentRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id, id,
type: null, type: EnvelopeType.DOCUMENT,
userId, userId,
teamId, teamId,
}); });
@ -62,13 +62,13 @@ export const createEnvelopeRecipients = async ({
if (!envelope) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found', message: 'Document not found',
}); });
} }
if (envelope.completedAt) { if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope already complete', message: 'Document already complete',
}); });
} }
@ -112,7 +112,6 @@ export const createEnvelopeRecipients = async ({
}); });
// Handle recipient created audit log. // Handle recipient created audit log.
if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED, type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
@ -128,7 +127,6 @@ export const createEnvelopeRecipients = async ({
}, },
}), }),
}); });
}
return createdRecipient; return createdRecipient;
}), }),

View File

@ -0,0 +1,115 @@
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface CreateTemplateRecipientsOptions {
userId: number;
teamId: number;
templateId: number;
recipients: {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
}[];
}
export const createTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients: recipientsToCreate,
}: CreateTemplateRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const template = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
team: {
select: {
organisation: {
select: {
organisationClaim: true,
},
},
},
},
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientsHaveActionAuth = recipientsToCreate.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
}));
const createdRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
normalizedRecipients.map(async (recipient) => {
const authOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth ?? [],
actionAuth: recipient.actionAuth ?? [],
});
const createdRecipient = await tx.recipient.create({
data: {
envelopeId: template.id,
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
authOptions,
},
});
return createdRecipient;
}),
);
});
return {
recipients: createdRecipients.map((recipient) =>
mapRecipientToLegacyRecipient(recipient, template),
),
};
};

View File

@ -14,27 +14,26 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { canRecipientBeModified } from '../../utils/recipients';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { buildTeamWhereQuery } from '../../utils/teams'; import { buildTeamWhereQuery } from '../../utils/teams';
import { getEmailContext } from '../email/get-email-context'; import { getEmailContext } from '../email/get-email-context';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface DeleteEnvelopeRecipientOptions { export interface DeleteDocumentRecipientOptions {
userId: number; userId: number;
teamId: number; teamId: number;
recipientId: number; recipientId: number;
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const deleteEnvelopeRecipient = async ({ export const deleteDocumentRecipient = async ({
userId, userId,
teamId, teamId,
recipientId, recipientId,
requestMetadata, requestMetadata,
}: DeleteEnvelopeRecipientOptions) => { }: DeleteDocumentRecipientOptions) => {
const envelope = await prisma.envelope.findFirst({ const envelope = await prisma.envelope.findFirst({
where: { where: {
type: EnvelopeType.DOCUMENT,
recipients: { recipients: {
some: { some: {
id: recipientId, id: recipientId,
@ -49,9 +48,6 @@ export const deleteEnvelopeRecipient = async ({
where: { where: {
id: recipientId, id: recipientId,
}, },
include: {
fields: true,
},
}, },
}, },
}); });
@ -93,24 +89,7 @@ export const deleteEnvelopeRecipient = async ({
}); });
} }
if (!canRecipientBeModified(recipientToDelete, recipientToDelete.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient has already interacted with the document.',
});
}
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelope.id,
},
type: null,
userId,
teamId,
});
const deletedRecipient = await prisma.$transaction(async (tx) => { const deletedRecipient = await prisma.$transaction(async (tx) => {
if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED, type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
@ -124,12 +103,10 @@ export const deleteEnvelopeRecipient = async ({
}, },
}), }),
}); });
}
return await tx.recipient.delete({ return await tx.recipient.delete({
where: { where: {
id: recipientId, id: recipientId,
envelope: envelopeWhereInput,
}, },
}); });
}); });
@ -139,11 +116,7 @@ export const deleteEnvelopeRecipient = async ({
).recipientRemoved; ).recipientRemoved;
// Send email to deleted recipient. // Send email to deleted recipient.
if ( if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) {
recipientToDelete.sendStatus === SendStatus.SENT &&
isRecipientRemovedEmailEnabled &&
envelope.type === EnvelopeType.DOCUMENT
) {
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(RecipientRemovedFromDocumentTemplate, { const template = createElement(RecipientRemovedFromDocumentTemplate, {

View File

@ -0,0 +1,58 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface DeleteTemplateRecipientOptions {
userId: number;
teamId: number;
recipientId: number;
}
export const deleteTemplateRecipient = async ({
userId,
teamId,
recipientId,
}: DeleteTemplateRecipientOptions): Promise<void> => {
const recipientToDelete = await prisma.recipient.findFirst({
where: {
id: recipientId,
envelope: {
type: EnvelopeType.TEMPLATE,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
});
if (!recipientToDelete) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: recipientToDelete.envelopeId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
await prisma.recipient.delete({
where: {
id: recipientId,
envelope: envelopeWhereInput,
},
});
};

View File

@ -1,4 +1,5 @@
import { EnvelopeType, RecipientRole, SendStatus, SigningStatus } from '@prisma/client'; import { EnvelopeType, RecipientRole } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth'; import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
@ -15,38 +16,29 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractLegacyIds } from '../../universal/id'; import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
import { type EnvelopeIdOptions } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields'; import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientBeModified } from '../../utils/recipients'; import { canRecipientBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateEnvelopeRecipientsOptions { export interface UpdateDocumentRecipientsOptions {
userId: number; userId: number;
teamId: number; teamId: number;
id: EnvelopeIdOptions; id: EnvelopeIdOptions;
recipients: { recipients: RecipientData[];
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
}[];
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const updateEnvelopeRecipients = async ({ export const updateDocumentRecipients = async ({
userId, userId,
teamId, teamId,
id, id,
recipients, recipients,
requestMetadata, requestMetadata,
}: UpdateEnvelopeRecipientsOptions) => { }: UpdateDocumentRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id, id,
type: null, type: EnvelopeType.DOCUMENT,
userId, userId,
teamId, teamId,
}); });
@ -70,13 +62,13 @@ export const updateEnvelopeRecipients = async ({
if (!envelope) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found', message: 'Document not found',
}); });
} }
if (envelope.completedAt) { if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope already complete', message: 'Document already complete',
}); });
} }
@ -168,10 +160,9 @@ export const updateEnvelopeRecipients = async ({
}); });
} }
// Handle recipient updated audit log.
if (envelope.type === EnvelopeType.DOCUMENT) {
const changes = diffRecipientChanges(originalRecipient, updatedRecipient); const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
// Handle recipient updated audit log.
if (changes.length > 0) { if (changes.length > 0) {
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({
@ -188,7 +179,6 @@ export const updateEnvelopeRecipients = async ({
}), }),
}); });
} }
}
return updatedRecipient; return updatedRecipient;
}), }),
@ -198,8 +188,19 @@ export const updateEnvelopeRecipients = async ({
return { return {
recipients: updatedRecipients.map((recipient) => ({ recipients: updatedRecipients.map((recipient) => ({
...recipient, ...recipient,
...extractLegacyIds(envelope), documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
templateId: null,
fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)), fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)),
})), })),
}; };
}; };
type RecipientData = {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
};

View File

@ -0,0 +1,168 @@
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateTemplateRecipientsOptions {
userId: number;
teamId: number;
templateId: number;
recipients: {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
}[];
}
export const updateTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients,
}: UpdateTemplateRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
team: {
select: {
organisation: {
select: {
organisationClaim: true,
},
},
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientsHaveActionAuth = recipients.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
const recipientsToUpdate = recipients.map((recipient) => {
const originalRecipient = envelope.recipients.find(
(existingRecipient) => existingRecipient.id === recipient.id,
);
if (!originalRecipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Recipient with id ${recipient.id} not found`,
});
}
return {
originalRecipient,
recipientUpdateData: recipient,
};
});
const updatedRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
recipientsToUpdate.map(async ({ originalRecipient, recipientUpdateData }) => {
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
if (
recipientUpdateData.actionAuth !== undefined ||
recipientUpdateData.accessAuth !== undefined
) {
authOptions = createRecipientAuthOptions({
accessAuth: recipientUpdateData.accessAuth || authOptions.accessAuth,
actionAuth: recipientUpdateData.actionAuth || authOptions.actionAuth,
});
}
const mergedRecipient = {
...originalRecipient,
...recipientUpdateData,
};
const updatedRecipient = await tx.recipient.update({
where: {
id: originalRecipient.id,
envelopeId: envelope.id,
},
data: {
name: mergedRecipient.name,
email: mergedRecipient.email,
role: mergedRecipient.role,
signingOrder: mergedRecipient.signingOrder,
envelopeId: envelope.id,
sendStatus:
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
mergedRecipient.role === RecipientRole.CC
? SigningStatus.SIGNED
: SigningStatus.NOT_SIGNED,
authOptions,
},
include: {
fields: true,
},
});
// Clear all fields if the recipient role is changed to a type that cannot have fields.
if (
originalRecipient.role !== updatedRecipient.role &&
(updatedRecipient.role === RecipientRole.CC ||
updatedRecipient.role === RecipientRole.VIEWER)
) {
await tx.field.deleteMany({
where: {
recipientId: updatedRecipient.id,
},
});
}
return updatedRecipient;
}),
);
});
return {
recipients: updatedRecipients.map((recipient) => ({
...recipient,
documentId: null,
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)),
})),
};
};

View File

@ -37,8 +37,11 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
userId: true, userId: true,
teamId: true, teamId: true,
folderId: true, folderId: true,
templateId: true,
}).extend({ }).extend({
templateId: z
.number()
.nullish()
.describe('The ID of the template that the document was created from, if any.'),
documentMeta: DocumentMetaSchema.pick({ documentMeta: DocumentMetaSchema.pick({
signingOrder: true, signingOrder: true,
distributionMethod: true, distributionMethod: true,

View File

@ -188,7 +188,7 @@ export type TFieldMetaSchema = z.infer<typeof ZFieldMetaSchema>;
export const ZFieldAndMetaSchema = z.discriminatedUnion('type', [ export const ZFieldAndMetaSchema = z.discriminatedUnion('type', [
z.object({ z.object({
type: z.literal(FieldType.SIGNATURE), type: z.literal(FieldType.SIGNATURE),
fieldMeta: ZSignatureFieldMeta.optional(), fieldMeta: z.undefined(),
}), }),
z.object({ z.object({
type: z.literal(FieldType.FREE_SIGNATURE), type: z.literal(FieldType.FREE_SIGNATURE),

View File

@ -50,11 +50,6 @@ export const ZFieldSchema = FieldSchema.pick({
templateId: z.number().nullish(), templateId: z.number().nullish(),
}); });
export const ZEnvelopeFieldSchema = ZFieldSchema.omit({
documentId: true,
templateId: true,
});
export const ZFieldPageNumberSchema = z export const ZFieldPageNumberSchema = z
.number() .number()
.min(1) .min(1)
@ -74,30 +69,6 @@ export const ZFieldWidthSchema = z.number().min(1).describe('The width of the fi
export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.'); export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.');
export const ZClampedFieldPositionXSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based X coordinate where the field will be placed.');
export const ZClampedFieldPositionYSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based Y coordinate where the field will be placed.');
export const ZClampedFieldWidthSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based width of the field on the page.');
export const ZClampedFieldHeightSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based height of the field on the page.');
// --------------------------------------------- // ---------------------------------------------
const PrismaDecimalSchema = z.preprocess( const PrismaDecimalSchema = z.preprocess(

View File

@ -95,18 +95,3 @@ export const ZRecipientManySchema = RecipientSchema.pick({
documentId: z.number().nullish(), documentId: z.number().nullish(),
templateId: z.number().nullish(), templateId: z.number().nullish(),
}); });
export const ZEnvelopeRecipientSchema = ZRecipientSchema.omit({
documentId: true,
templateId: true,
});
export const ZEnvelopeRecipientLiteSchema = ZRecipientLiteSchema.omit({
documentId: true,
templateId: true,
});
export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
documentId: true,
templateId: true,
});

View File

@ -30,5 +30,3 @@ export const symmetricDecrypt = ({ key, data }: SymmetricDecryptOptions) => {
return chacha.decrypt(dataAsBytes); return chacha.decrypt(dataAsBytes);
}; };
export { sha256 };

View File

@ -153,11 +153,6 @@ export const createFieldHoverInteraction = ({
const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover; const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover;
fieldGroup.on('mouseover', () => { fieldGroup.on('mouseover', () => {
const layer = fieldRect.getLayer();
if (!layer) {
return;
}
new Konva.Tween({ new Konva.Tween({
node: fieldRect, node: fieldRect,
duration: 0.3, duration: 0.3,
@ -166,11 +161,6 @@ export const createFieldHoverInteraction = ({
}); });
fieldGroup.on('mouseout', () => { fieldGroup.on('mouseout', () => {
const layer = fieldRect.getLayer();
if (!layer) {
return;
}
new Konva.Tween({ new Konva.Tween({
node: fieldRect, node: fieldRect,
duration: 0.3, duration: 0.3,
@ -179,11 +169,6 @@ export const createFieldHoverInteraction = ({
}); });
fieldGroup.on('transformstart', () => { fieldGroup.on('transformstart', () => {
const layer = fieldRect.getLayer();
if (!layer) {
return;
}
new Konva.Tween({ new Konva.Tween({
node: fieldRect, node: fieldRect,
duration: 0.3, duration: 0.3,
@ -192,11 +177,6 @@ export const createFieldHoverInteraction = ({
}); });
fieldGroup.on('transformend', () => { fieldGroup.on('transformend', () => {
const layer = fieldRect.getLayer();
if (!layer) {
return;
}
new Konva.Tween({ new Konva.Tween({
node: fieldRect, node: fieldRect,
duration: 0.3, duration: 0.3,

View File

@ -3,7 +3,6 @@ import { match } from 'ts-pattern';
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
import type { TCheckboxFieldMeta } from '../../types/field-meta'; import type { TCheckboxFieldMeta } from '../../types/field-meta';
import { parseCheckboxCustomText } from '../../utils/fields';
import { import {
createFieldHoverInteraction, createFieldHoverInteraction,
konvaTextFill, konvaTextFill,
@ -130,7 +129,7 @@ export const renderCheckboxFieldElement = (
pageLayer.batchDraw(); pageLayer.batchDraw();
}); });
const checkedValues: number[] = field.customText ? parseCheckboxCustomText(field.customText) : []; const checkedValues: number[] = field.customText ? JSON.parse(field.customText) : [];
checkboxValues.forEach(({ value, checked }, index) => { checkboxValues.forEach(({ value, checked }, index) => {
const isCheckboxChecked = match(mode) const isCheckboxChecked = match(mode)
@ -170,7 +169,7 @@ export const renderCheckboxFieldElement = (
width: itemSize, width: itemSize,
height: itemSize, height: itemSize,
stroke: '#374151', stroke: '#374151',
strokeWidth: 1.5, strokeWidth: 2,
cornerRadius: 2, cornerRadius: 2,
fill: 'white', fill: 'white',
}); });

View File

@ -159,7 +159,7 @@ export const renderRadioFieldElement = (
y: itemInputY, y: itemInputY,
radius: calculateRadioSize(fontSize) / 2, radius: calculateRadioSize(fontSize) / 2,
stroke: '#374151', stroke: '#374151',
strokeWidth: 1.5, strokeWidth: 2,
fill: 'white', fill: 'white',
}); });

View File

@ -7,13 +7,7 @@ export type GetFileOptions = {
data: string; data: string;
}; };
/** export const getFile = async ({ type, data }: GetFileOptions) => {
* KEPT FOR POSTERITY, SHOULD BE REMOVED IN THE FUTURE
* DO NOT USE OR I WILL FIRE YOU
*
* - Lucas, 2025-11-04
*/
const getFile = async ({ type, data }: GetFileOptions) => {
return await match(type) return await match(type)
.with(DocumentDataType.BYTES, () => getFileFromBytes(data)) .with(DocumentDataType.BYTES, () => getFileFromBytes(data))
.with(DocumentDataType.BYTES_64, () => getFileFromBytes64(data)) .with(DocumentDataType.BYTES_64, () => getFileFromBytes64(data))

View File

@ -7,7 +7,6 @@ import { env } from '@documenso/lib/utils/env';
import { AppError } from '../../errors/app-error'; import { AppError } from '../../errors/app-error';
import { createDocumentData } from '../../server-only/document-data/create-document-data'; import { createDocumentData } from '../../server-only/document-data/create-document-data';
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
import { uploadS3File } from './server-actions'; import { uploadS3File } from './server-actions';
type File = { type File = {
@ -44,28 +43,6 @@ export const putPdfFileServerSide = async (file: File) => {
return await createDocumentData({ type, data }); return await createDocumentData({ type, data });
}; };
/**
* Uploads a pdf file and normalizes it.
*/
export const putNormalizedPdfFileServerSide = async (file: File) => {
const buffer = Buffer.from(await file.arrayBuffer());
const normalized = await normalizePdf(buffer);
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
const documentData = await putFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(normalized),
});
return await createDocumentData({
type: documentData.type,
data: documentData.data,
});
};
/** /**
* Uploads a file to the appropriate storage location. * Uploads a file to the appropriate storage location.
*/ */

View File

@ -104,6 +104,7 @@ export const extractFieldInsertionValues = ({
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta); const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true); const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
// Todo
if (errors.length > 0) { if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, { throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid number', message: 'Invalid number',
@ -126,6 +127,7 @@ export const extractFieldInsertionValues = ({
const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta); const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta);
const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true); const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true);
// Todo
if (errors.length > 0) { if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, { throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid email', message: 'Invalid email',
@ -187,6 +189,7 @@ export const extractFieldInsertionValues = ({
(sign) => sign.label === validationRule, (sign) => sign.label === validationRule,
); );
// Todo: Envelopes - Test this.
if (checkboxValidationRule) { if (checkboxValidationRule) {
const isValid = validateCheckboxLength( const isValid = validateCheckboxLength(
selectedValues.length, selectedValues.length,
@ -221,6 +224,7 @@ export const extractFieldInsertionValues = ({
const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta); const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true); const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true);
// Todo: Envelopes
if (errors.length > 0) { if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, { throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid dropdown value', message: 'Invalid dropdown value',

View File

@ -81,10 +81,6 @@ export const mapFieldToLegacyField = (
}; };
export const parseCheckboxCustomText = (customText: string): number[] => { export const parseCheckboxCustomText = (customText: string): number[] => {
if (!customText) {
return [];
}
return JSON.parse(customText); return JSON.parse(customText);
}; };

View File

@ -21,14 +21,14 @@
"seed": "tsx ./seed-database.ts" "seed": "tsx ./seed-database.ts"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.18.0", "@prisma/client": "^6.8.2",
"kysely": "0.26.3", "kysely": "0.26.3",
"prisma": "^6.18.0", "prisma": "^6.8.2",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"prisma-json-types-generator": "^3.6.2", "prisma-json-types-generator": "^3.2.2",
"ts-pattern": "^5.0.6", "ts-pattern": "^5.0.6",
"zod-prisma-types": "3.3.5" "zod-prisma-types": "3.2.4"
}, },
"devDependencies": { "devDependencies": {
"dotenv": "^16.5.0", "dotenv": "^16.5.0",

View File

@ -134,8 +134,8 @@ model Passkey {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) updatedAt DateTime @default(now())
lastUsedAt DateTime? lastUsedAt DateTime?
credentialId Bytes /// @zod.custom.use(z.instanceof(Uint8Array)) credentialId Bytes
credentialPublicKey Bytes /// @zod.custom.use(z.instanceof(Uint8Array)) credentialPublicKey Bytes
counter BigInt counter BigInt
credentialDeviceType String credentialDeviceType String
credentialBackedUp Boolean credentialBackedUp Boolean

View File

@ -1,22 +1,11 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { formatAlignmentTestFields } from '@documenso/app-tests/constants/field-alignment-pdf';
import { FIELD_META_TEST_FIELDS } from '@documenso/app-tests/constants/field-meta-pdf';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { incrementDocumentId } from '@documenso/lib/server-only/envelope/increment-id'; import { incrementDocumentId } from '@documenso/lib/server-only/envelope/increment-id';
import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '..'; import { prisma } from '..';
import { import { DocumentDataType, DocumentSource, EnvelopeType } from '../client';
DocumentDataType,
DocumentSource,
DocumentStatus,
EnvelopeType,
ReadStatus,
SendStatus,
SigningStatus,
} from '../client';
import { seedPendingDocument } from './documents'; import { seedPendingDocument } from './documents';
import { seedDirectTemplate, seedTemplate } from './templates'; import { seedDirectTemplate, seedTemplate } from './templates';
import { seedUser } from './users'; import { seedUser } from './users';
@ -166,6 +155,7 @@ export const seedDatabase = async () => {
userId: exampleUser.user.id, userId: exampleUser.user.id,
teamId: exampleUser.team.id, teamId: exampleUser.team.id,
}), }),
seedTemplate({ seedTemplate({
title: 'Template 1', title: 'Template 1',
userId: adminUser.user.id, userId: adminUser.user.id,
@ -176,185 +166,5 @@ export const seedDatabase = async () => {
userId: adminUser.user.id, userId: adminUser.user.id,
teamId: adminUser.team.id, teamId: adminUser.team.id,
}), }),
seedAlignmentTestDocument({
userId: exampleUser.user.id,
teamId: exampleUser.team.id,
recipientName: exampleUser.user.name || '',
recipientEmail: exampleUser.user.email,
insertFields: false,
status: DocumentStatus.DRAFT,
}),
seedAlignmentTestDocument({
userId: exampleUser.user.id,
teamId: exampleUser.team.id,
recipientName: exampleUser.user.name || '',
recipientEmail: exampleUser.user.email,
insertFields: true,
status: DocumentStatus.PENDING,
}),
seedAlignmentTestDocument({
userId: adminUser.user.id,
teamId: adminUser.team.id,
recipientName: adminUser.user.name || '',
recipientEmail: adminUser.user.email,
insertFields: false,
status: DocumentStatus.DRAFT,
}),
seedAlignmentTestDocument({
userId: adminUser.user.id,
teamId: adminUser.team.id,
recipientName: adminUser.user.name || '',
recipientEmail: adminUser.user.email,
insertFields: true,
status: DocumentStatus.PENDING,
}),
]); ]);
}; };
export const seedAlignmentTestDocument = async ({
userId,
teamId,
recipientName,
recipientEmail,
insertFields,
status,
}: {
userId: number;
teamId: number;
recipientName: string;
recipientEmail: string;
insertFields: boolean;
status: DocumentStatus;
}) => {
const alignmentPdf = fs
.readFileSync(path.join(__dirname, '../../../assets/field-font-alignment.pdf'))
.toString('base64');
const fieldMetaPdf = fs
.readFileSync(path.join(__dirname, '../../../assets/field-meta.pdf'))
.toString('base64');
const alignmentDocumentData = await createDocumentData({ documentData: alignmentPdf });
const fieldMetaDocumentData = await createDocumentData({ documentData: fieldMetaPdf });
const documentId = await incrementDocumentId();
const documentMeta = await prisma.documentMeta.create({
data: {},
});
const createdEnvelope = await prisma.envelope.create({
data: {
id: prefixedId('envelope'),
secondaryId: documentId.formattedDocumentId,
internalVersion: 2,
type: EnvelopeType.DOCUMENT,
documentMetaId: documentMeta.id,
source: DocumentSource.DOCUMENT,
title: `Envelope Full Field Test`,
status,
envelopeItems: {
createMany: {
data: [
{
id: prefixedId('envelope_item'),
title: `alignment-pdf`,
documentDataId: alignmentDocumentData.id,
order: 1,
},
{
id: prefixedId('envelope_item'),
title: `field-meta-pdf`,
documentDataId: fieldMetaDocumentData.id,
order: 2,
},
],
},
},
userId,
teamId,
recipients: {
create: {
name: recipientName,
email: recipientEmail,
token: nanoid(),
sendStatus: status === 'DRAFT' ? SendStatus.NOT_SENT : SendStatus.SENT,
signingStatus: status === 'COMPLETED' ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
readStatus: status !== 'DRAFT' ? ReadStatus.OPENED : ReadStatus.NOT_OPENED,
},
},
},
include: {
recipients: true,
envelopeItems: true,
},
});
const { id, recipients, envelopeItems } = createdEnvelope;
const recipientId = recipients[0].id;
const envelopeItemAlignmentItem = envelopeItems.find((item) => item.order === 1)?.id;
const envelopeItemFieldMetaItem = envelopeItems.find((item) => item.order === 2)?.id;
if (!envelopeItemAlignmentItem || !envelopeItemFieldMetaItem) {
throw new Error('Envelope item not found');
}
await Promise.all(
formatAlignmentTestFields.map(async (field) => {
await prisma.field.create({
data: {
...field,
recipientId,
envelopeItemId: envelopeItemAlignmentItem,
envelopeId: id,
customText: insertFields ? field.customText : '',
inserted: insertFields,
signature: field.signature
? {
create: {
recipientId,
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
typedSignature: isBase64Image(field.signature) ? null : field.signature,
},
}
: undefined,
},
});
}),
);
await Promise.all(
FIELD_META_TEST_FIELDS.map(async (field) => {
await prisma.field.create({
data: {
...field,
recipientId,
envelopeItemId: envelopeItemFieldMetaItem,
envelopeId: id,
customText: insertFields ? field.customText : '',
inserted: insertFields,
signature: field.signature
? {
create: {
recipientId,
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
typedSignature: isBase64Image(field.signature) ? null : field.signature,
},
}
: undefined,
},
});
}),
);
return await prisma.envelope.findFirstOrThrow({
where: {
id: createdEnvelope.id,
},
include: {
recipients: true,
envelopeItems: true,
},
});
};

View File

@ -1,23 +1,17 @@
import { import { createTRPCClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
createTRPCClient, import SuperJSON from 'superjson';
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router'; import type { AppRouter } from '../server/router';
import { dataTransformer } from '../utils/data-transformer';
export const trpc = createTRPCClient<AppRouter>({ export const trpc = createTRPCClient<AppRouter>({
links: [ links: [
splitLink({ splitLink({
condition: (op) => op.context.skipBatch === true || isNonJsonSerializable(op.input), condition: (op) => op.context.skipBatch === true,
true: httpLink({ true: httpLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
transformer: dataTransformer, transformer: SuperJSON,
headers: (opts) => { headers: (opts) => {
if (typeof opts.op.context.teamId === 'string') { if (typeof opts.op.context.teamId === 'string') {
return { return {
@ -30,7 +24,7 @@ export const trpc = createTRPCClient<AppRouter>({
}), }),
false: httpBatchLink({ false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
transformer: dataTransformer, transformer: SuperJSON,
headers: (opts) => { headers: (opts) => {
const operationWithTeamId = opts.opList.find( const operationWithTeamId = opts.opList.find(
(op) => op.context.teamId && typeof op.context.teamId === 'string', (op) => op.context.teamId && typeof op.context.teamId === 'string',

View File

@ -12,21 +12,15 @@
"dependencies": { "dependencies": {
"@documenso/lib": "*", "@documenso/lib": "*",
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@tanstack/react-query": "5.90.5", "@tanstack/react-query": "5.59.15",
"@trpc/client": "11.7.0", "@trpc/client": "11.0.0-rc.648",
"@trpc/react-query": "11.7.0", "@trpc/react-query": "11.0.0-rc.648",
"@trpc/server": "11.7.0", "@trpc/server": "11.0.0-rc.648",
"@ts-rest/core": "^3.52.0", "@ts-rest/core": "^3.30.5",
"formidable": "^3.5.4",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"superjson": "^2.2.5", "superjson": "^1.13.1",
"trpc-to-openapi": "2.4.0", "trpc-to-openapi": "2.0.4",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76", "zod": "3.24.1"
"zod-form-data": "^2.0.8",
"zod-openapi": "^4.2.4"
},
"devDependencies": {
"@types/formidable": "^3.4.6"
} }
} }

View File

@ -1,13 +1,13 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, httpLink, isNonJsonSerializable, splitLink } from '@trpc/client'; import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query'; import { createTRPCReact } from '@trpc/react-query';
import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router'; import type { AppRouter } from '../server/router';
import { dataTransformer } from '../utils/data-transformer';
export { getQueryKey } from '@trpc/react-query'; export { getQueryKey } from '@trpc/react-query';
@ -44,16 +44,16 @@ export function TrpcProvider({ children, headers }: TrpcProviderProps) {
trpc.createClient({ trpc.createClient({
links: [ links: [
splitLink({ splitLink({
condition: (op) => op.context.skipBatch === true || isNonJsonSerializable(op.input), condition: (op) => op.context.skipBatch === true,
true: httpLink({ true: httpLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
headers, headers,
transformer: dataTransformer, transformer: SuperJSON,
}), }),
false: httpBatchLink({ false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
headers, headers,
transformer: dataTransformer, transformer: SuperJSON,
}), }),
}), }),
], ],

View File

@ -1,136 +0,0 @@
import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateDocumentFormDataRequestSchema,
ZCreateDocumentFormDataResponseSchema,
createDocumentFormDataMeta,
} from './create-document-formdata.types';
/**
* Temporary endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
*/
export const createDocumentFormDataRoute = authenticatedProcedure
.meta(createDocumentFormDataMeta)
.input(ZCreateDocumentFormDataRequestSchema)
.output(ZCreateDocumentFormDataResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { payload, file } = input;
const {
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
meta,
folderId,
attachments,
} = payload;
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
const documentData = await putPdfFileServerSide(file);
const createdEnvelope = await createEnvelope({
userId: ctx.user.id,
teamId,
normalizePdf: false, // Not normalizing because of presigned URL.
internalVersion: 1,
data: {
type: EnvelopeType.DOCUMENT,
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients: (recipients || []).map((recipient) => ({
...recipient,
fields: (recipient.fields || []).map((field) => ({
...field,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
documentDataId: documentData.id,
})),
})),
folderId,
envelopeItems: [
{
// If you ever allow more than 1 in this endpoint, make sure to use `maximumEnvelopeItemCount` to limit it.
documentDataId: documentData.id,
},
],
},
attachments,
meta: {
...meta,
emailSettings: meta?.emailSettings ?? undefined,
},
requestMetadata: ctx.metadata,
});
const envelopeItems = await prisma.envelopeItem.findMany({
where: {
envelopeId: createdEnvelope.id,
},
include: {
documentData: true,
},
});
const legacyDocumentId = mapSecondaryIdToDocumentId(createdEnvelope.secondaryId);
const firstDocumentData = envelopeItems[0].documentData;
if (!firstDocumentData) {
throw new Error('Document data not found');
}
return {
document: {
...createdEnvelope,
envelopeId: createdEnvelope.id,
documentDataId: firstDocumentData.id,
documentData: {
...firstDocumentData,
envelopeItemId: envelopeItems[0].id,
},
documentMeta: {
...createdEnvelope.documentMeta,
documentId: legacyDocumentId,
},
id: legacyDocumentId,
fields: createdEnvelope.fields.map((field) => ({
...field,
documentId: legacyDocumentId,
templateId: null,
})),
recipients: createdEnvelope.recipients.map((recipient) => ({
...recipient,
documentId: legacyDocumentId,
templateId: null,
})),
},
folder: createdEnvelope.folder, // Todo: Remove this prior to api-v2 release.
};
});

View File

@ -1,97 +0,0 @@
import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { ZDocumentSchema } from '@documenso/lib/types/document';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentExternalIdSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from './schema';
export const createDocumentFormDataMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/create/formdata',
contentTypes: ['multipart/form-data'],
summary: 'Create document',
description: 'Create a document using form data.',
tags: ['Document'],
},
};
const ZCreateDocumentFormDataPayloadRequestSchema = z.object({
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(),
folderId: z
.string()
.describe(
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
)
.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
)
.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
meta: ZDocumentMetaCreateSchema.optional(),
});
// !: Can't use zfd.formData() here because it receives `undefined`
// !: somewhere in the pipeline of our openapi schema generation and throws
// !: an error.
export const ZCreateDocumentFormDataRequestSchema = zodFormData({
payload: zfd.json(ZCreateDocumentFormDataPayloadRequestSchema),
file: zfd.file(),
});
export const ZCreateDocumentFormDataResponseSchema = z.object({
document: ZDocumentSchema,
});
export type TCreateDocumentFormDataRequest = z.infer<typeof ZCreateDocumentFormDataRequestSchema>;
export type TCreateDocumentFormDataResponse = z.infer<typeof ZCreateDocumentFormDataResponseSchema>;

View File

@ -3,28 +3,20 @@ import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { authenticatedProcedure } from '../trpc'; import { authenticatedProcedure } from '../trpc';
import { import {
ZCreateDocumentRequestSchema, ZCreateDocumentRequestSchema,
ZCreateDocumentResponseSchema, ZCreateDocumentResponseSchema,
createDocumentMeta,
} from './create-document.types'; } from './create-document.types';
export const createDocumentRoute = authenticatedProcedure export const createDocumentRoute = authenticatedProcedure
.meta(createDocumentMeta) .input(ZCreateDocumentRequestSchema) // Note: Before releasing this to public, update the response schema to be correct.
.input(ZCreateDocumentRequestSchema)
.output(ZCreateDocumentResponseSchema) .output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx; const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId, attachments } = input;
const { payload, file } = input;
const { title, timezone, folderId, attachments } = payload;
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -63,7 +55,6 @@ export const createDocumentRoute = authenticatedProcedure
}); });
return { return {
envelopeId: document.id, legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId),
id: mapSecondaryIdToDocumentId(document.secondaryId),
}; };
}); });

View File

@ -1,26 +1,23 @@
import { z } from 'zod'; import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { zodFormData } from '../../utils/zod-form-data';
import type { TrpcRouteMeta } from '../trpc';
import { ZDocumentTitleSchema } from './schema'; import { ZDocumentTitleSchema } from './schema';
export const createDocumentMeta: TrpcRouteMeta = { // Currently not in use until we allow passthrough documents on create.
openapi: { // export const createDocumentMeta: TrpcRouteMeta = {
method: 'POST', // openapi: {
path: '/document/create', // method: 'POST',
contentTypes: ['multipart/form-data'], // path: '/document/create',
summary: 'Create document', // summary: 'Create document',
description: 'Create a document using form data.', // tags: ['Document'],
tags: ['Document'], // },
}, // };
};
export const ZCreateDocumentPayloadSchema = z.object({ export const ZCreateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
documentDataId: z.string().min(1),
timezone: ZDocumentMetaTimezoneSchema.optional(), timezone: ZDocumentMetaTimezoneSchema.optional(),
folderId: z.string().describe('The ID of the folder to create the document in').optional(), folderId: z.string().describe('The ID of the folder to create the document in').optional(),
attachments: z attachments: z
@ -34,16 +31,9 @@ export const ZCreateDocumentPayloadSchema = z.object({
.optional(), .optional(),
}); });
export const ZCreateDocumentRequestSchema = zodFormData({
payload: zfd.json(ZCreateDocumentPayloadSchema),
file: zfd.file(),
});
export const ZCreateDocumentResponseSchema = z.object({ export const ZCreateDocumentResponseSchema = z.object({
envelopeId: z.string(), legacyDocumentId: z.number(),
id: z.number(),
}); });
export type TCreateDocumentPayloadSchema = z.infer<typeof ZCreateDocumentPayloadSchema>;
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>; export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>; export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;

View File

@ -5,7 +5,6 @@ import { deleteAttachmentRoute } from './attachment/delete-attachment';
import { findAttachmentsRoute } from './attachment/find-attachments'; import { findAttachmentsRoute } from './attachment/find-attachments';
import { updateAttachmentRoute } from './attachment/update-attachment'; import { updateAttachmentRoute } from './attachment/update-attachment';
import { createDocumentRoute } from './create-document'; import { createDocumentRoute } from './create-document';
import { createDocumentFormDataRoute } from './create-document-formdata';
import { createDocumentTemporaryRoute } from './create-document-temporary'; import { createDocumentTemporaryRoute } from './create-document-temporary';
import { deleteDocumentRoute } from './delete-document'; import { deleteDocumentRoute } from './delete-document';
import { distributeDocumentRoute } from './distribute-document'; import { distributeDocumentRoute } from './distribute-document';
@ -41,7 +40,6 @@ export const documentRouter = router({
// Temporary v2 beta routes to be removed once V2 is fully released. // Temporary v2 beta routes to be removed once V2 is fully released.
download: downloadDocumentRoute, download: downloadDocumentRoute,
createDocumentTemporary: createDocumentTemporaryRoute, createDocumentTemporary: createDocumentTemporaryRoute,
createDocumentFormData: createDocumentFormDataRoute,
// Internal document routes for custom frontend requests. // Internal document routes for custom frontend requests.
getDocumentByToken: getDocumentByTokenRoute, getDocumentByToken: getDocumentByTokenRoute,

View File

@ -13,7 +13,7 @@ export const createAttachmentRoute = authenticatedProcedure
path: '/envelope/attachment/create', path: '/envelope/attachment/create',
summary: 'Create attachment', summary: 'Create attachment',
description: 'Create a new attachment for an envelope', description: 'Create a new attachment for an envelope',
tags: ['Envelope Attachments'], tags: ['Envelope'],
}, },
}) })
.input(ZCreateAttachmentRequestSchema) .input(ZCreateAttachmentRequestSchema)

View File

@ -13,7 +13,7 @@ export const deleteAttachmentRoute = authenticatedProcedure
path: '/envelope/attachment/delete', path: '/envelope/attachment/delete',
summary: 'Delete attachment', summary: 'Delete attachment',
description: 'Delete an attachment from an envelope', description: 'Delete an attachment from an envelope',
tags: ['Envelope Attachments'], tags: ['Envelope'],
}, },
}) })
.input(ZDeleteAttachmentRequestSchema) .input(ZDeleteAttachmentRequestSchema)

View File

@ -2,20 +2,20 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id'; import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id';
import { findAttachmentsByToken } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-token'; import { findAttachmentsByToken } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-token';
import { maybeAuthenticatedProcedure } from '../../trpc'; import { procedure } from '../../trpc';
import { import {
ZFindAttachmentsRequestSchema, ZFindAttachmentsRequestSchema,
ZFindAttachmentsResponseSchema, ZFindAttachmentsResponseSchema,
} from './find-attachments.types'; } from './find-attachments.types';
export const findAttachmentsRoute = maybeAuthenticatedProcedure export const findAttachmentsRoute = procedure
.meta({ .meta({
openapi: { openapi: {
method: 'GET', method: 'GET',
path: '/envelope/attachment', path: '/envelope/attachment',
summary: 'Find attachments', summary: 'Find attachments',
description: 'Find all attachments for an envelope', description: 'Find all attachments for an envelope',
tags: ['Envelope Attachments'], tags: ['Envelope'],
}, },
}) })
.input(ZFindAttachmentsRequestSchema) .input(ZFindAttachmentsRequestSchema)

View File

@ -13,7 +13,7 @@ export const updateAttachmentRoute = authenticatedProcedure
path: '/envelope/attachment/update', path: '/envelope/attachment/update',
summary: 'Update attachment', summary: 'Update attachment',
description: 'Update an existing attachment', description: 'Update an existing attachment',
tags: ['Envelope Attachments'], tags: ['Envelope'],
}, },
}) })
.input(ZUpdateAttachmentRequestSchema) .input(ZUpdateAttachmentRequestSchema)

View File

@ -13,21 +13,11 @@ import {
} from './create-envelope-items.types'; } from './create-envelope-items.types';
export const createEnvelopeItemsRoute = authenticatedProcedure export const createEnvelopeItemsRoute = authenticatedProcedure
// Todo: Envelopes - Pending direct uploads
.meta({
openapi: {
method: 'POST',
path: '/envelope/item/create-many',
summary: 'Create envelope items',
description: 'Create multiple envelope items for an envelope',
tags: ['Envelope Items'],
},
})
.input(ZCreateEnvelopeItemsRequestSchema) .input(ZCreateEnvelopeItemsRequestSchema)
.output(ZCreateEnvelopeItemsResponseSchema) .output(ZCreateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx; const { user, teamId, metadata } = ctx;
const { envelopeId, data: items } = input; const { envelopeId, items } = input;
ctx.logger.info({ ctx.logger.info({
input: { input: {

View File

@ -7,7 +7,7 @@ import { ZDocumentTitleSchema } from '../document-router/schema';
export const ZCreateEnvelopeItemsRequestSchema = z.object({ export const ZCreateEnvelopeItemsRequestSchema = z.object({
envelopeId: z.string(), envelopeId: z.string(),
data: z items: z
.object({ .object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
documentDataId: z.string(), documentDataId: z.string(),

View File

@ -1,7 +1,6 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { authenticatedProcedure } from '../trpc'; import { authenticatedProcedure } from '../trpc';
import { import {
@ -10,21 +9,10 @@ import {
} from './create-envelope.types'; } from './create-envelope.types';
export const createEnvelopeRoute = authenticatedProcedure export const createEnvelopeRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/create',
summary: 'Create envelope',
tags: ['Envelope'],
},
})
.input(ZCreateEnvelopeRequestSchema) .input(ZCreateEnvelopeRequestSchema)
.output(ZCreateEnvelopeResponseSchema) .output(ZCreateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx; const { user, teamId } = ctx;
const { payload, files } = input;
const { const {
title, title,
type, type,
@ -32,12 +20,12 @@ export const createEnvelopeRoute = authenticatedProcedure
visibility, visibility,
globalAccessAuth, globalAccessAuth,
globalActionAuth, globalActionAuth,
formValues,
recipients, recipients,
folderId, folderId,
items,
meta, meta,
attachments, attachments,
} = payload; } = input;
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -57,62 +45,13 @@ export const createEnvelopeRoute = authenticatedProcedure
}); });
} }
if (files.length > maximumEnvelopeItemCount) { if (items.length > maximumEnvelopeItemCount) {
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`, message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
statusCode: 400, statusCode: 400,
}); });
} }
// For each file, stream to s3 and create the document data.
const envelopeItems = await Promise.all(
files.map(async (file) => {
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
return {
title: file.name,
documentDataId,
};
}),
);
const recipientsToCreate = recipients?.map((recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
accessAuth: recipient.accessAuth,
actionAuth: recipient.actionAuth,
fields: recipient.fields?.map((field) => {
let documentDataId: string | undefined = undefined;
if (typeof field.identifier === 'string') {
documentDataId = envelopeItems.find(
(item) => item.title === field.identifier,
)?.documentDataId;
}
if (typeof field.identifier === 'number') {
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
}
if (field.identifier === undefined) {
documentDataId = envelopeItems[0]?.documentDataId;
}
if (!documentDataId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document data not found',
});
}
return {
...field,
documentDataId,
};
}),
}));
const envelope = await createEnvelope({ const envelope = await createEnvelope({
userId: user.id, userId: user.id,
teamId, teamId,
@ -121,16 +60,16 @@ export const createEnvelopeRoute = authenticatedProcedure
type, type,
title, title,
externalId, externalId,
formValues,
visibility, visibility,
globalAccessAuth, globalAccessAuth,
globalActionAuth, globalActionAuth,
recipients: recipientsToCreate, recipients,
folderId, folderId,
envelopeItems, envelopeItems: items,
}, },
attachments, attachments,
meta, meta,
normalizePdf: true,
requestMetadata: ctx.metadata, requestMetadata: ctx.metadata,
}); });

View File

@ -1,6 +1,5 @@
import { EnvelopeType } from '@prisma/client'; import { EnvelopeType } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { import {
ZDocumentAccessAuthTypesSchema, ZDocumentAccessAuthTypesSchema,
@ -10,36 +9,32 @@ import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-va
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta'; import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { import {
ZClampedFieldHeightSchema, ZFieldHeightSchema,
ZClampedFieldPositionXSchema,
ZClampedFieldPositionYSchema,
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema, ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field'; } from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import { import {
ZDocumentExternalIdSchema, ZDocumentExternalIdSchema,
ZDocumentTitleSchema, ZDocumentTitleSchema,
ZDocumentVisibilitySchema, ZDocumentVisibilitySchema,
} from '../document-router/schema'; } from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema'; import { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
// Currently not in use until we allow passthrough documents on create. // Currently not in use until we allow passthrough documents on create.
export const createEnvelopeMeta: TrpcRouteMeta = { // export const createEnvelopeMeta: TrpcRouteMeta = {
openapi: { // openapi: {
method: 'POST', // method: 'POST',
path: '/envelope/create', // path: '/envelope/create',
contentTypes: ['multipart/form-data'], // summary: 'Create envelope',
summary: 'Create envelope', // tags: ['Envelope'],
description: 'Create a envelope using form data.', // },
tags: ['Envelope'], // };
},
};
export const ZCreateEnvelopePayloadSchema = z.object({ export const ZCreateEnvelopeRequestSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
type: z.nativeEnum(EnvelopeType), type: z.nativeEnum(EnvelopeType),
externalId: ZDocumentExternalIdSchema.optional(), externalId: ZDocumentExternalIdSchema.optional(),
@ -47,6 +42,12 @@ export const ZCreateEnvelopePayloadSchema = z.object({
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(), globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(), globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(), formValues: ZDocumentFormValuesSchema.optional(),
items: z
.object({
title: ZDocumentTitleSchema.optional(),
documentDataId: z.string(),
})
.array(),
folderId: z folderId: z
.string() .string()
.describe( .describe(
@ -58,17 +59,16 @@ export const ZCreateEnvelopePayloadSchema = z.object({
ZCreateRecipientSchema.extend({ ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and( fields: ZFieldAndMetaSchema.and(
z.object({ z.object({
identifier: z documentDataId: z
.union([z.string(), z.number()]) .string()
.describe( .describe(
'Either the filename or the index of the file that was uploaded to attach the field to.', 'The ID of the document data to create the field on. If empty, the first document data will be used.',
) ),
.optional(),
page: ZFieldPageNumberSchema, page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema, positionX: ZFieldPageXSchema,
positionY: ZClampedFieldPositionYSchema, positionY: ZFieldPageYSchema,
width: ZClampedFieldWidthSchema, width: ZFieldWidthSchema,
height: ZClampedFieldHeightSchema, height: ZFieldHeightSchema,
}), }),
) )
.array() .array()
@ -88,15 +88,9 @@ export const ZCreateEnvelopePayloadSchema = z.object({
.optional(), .optional(),
}); });
export const ZCreateEnvelopeRequestSchema = zodFormData({
payload: zfd.json(ZCreateEnvelopePayloadSchema),
files: zfd.repeatableOfType(zfd.file()),
});
export const ZCreateEnvelopeResponseSchema = z.object({ export const ZCreateEnvelopeResponseSchema = z.object({
id: z.string(), id: z.string(),
}); });
export type TCreateEnvelopePayload = z.infer<typeof ZCreateEnvelopePayloadSchema>;
export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>; export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>;
export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>; export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>;

View File

@ -12,15 +12,6 @@ import {
} from './delete-envelope-item.types'; } from './delete-envelope-item.types';
export const deleteEnvelopeItemRoute = authenticatedProcedure export const deleteEnvelopeItemRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/item/delete',
summary: 'Delete envelope item',
description: 'Delete an envelope item from an envelope',
tags: ['Envelope Items'],
},
})
.input(ZDeleteEnvelopeItemRequestSchema) .input(ZDeleteEnvelopeItemRequestSchema)
.output(ZDeleteEnvelopeItemResponseSchema) .output(ZDeleteEnvelopeItemResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

Some files were not shown because too many files have changed in this diff Show More