mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 19:21:39 +10:00
fix: merge conflicts
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
// import { numberFormatValues } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||
import { numberFormatValues } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||
|
||||
import type { TNumberFieldMeta as NumberFieldMeta } from '../types/field-meta';
|
||||
|
||||
export const validateNumberField = (
|
||||
@ -10,16 +11,16 @@ export const validateNumberField = (
|
||||
|
||||
const { minValue, maxValue, readOnly, required, numberFormat, fontSize } = fieldMeta || {};
|
||||
|
||||
const formatRegex: { [key: string]: RegExp } = {
|
||||
'123,456,789.00': /^(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d{1,2})?$/,
|
||||
'123.456.789,00': /^(?:\d{1,3}(?:\.\d{3})*|\d+)(?:,\d{1,2})?$/,
|
||||
'123456,789.00': /^(?:\d+)(?:,\d{1,3}(?:\.\d{1,2})?)?$/,
|
||||
};
|
||||
if (numberFormat) {
|
||||
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
||||
|
||||
const isValidFormat = numberFormat ? formatRegex[numberFormat].test(value) : true;
|
||||
if (!foundRegex) {
|
||||
errors.push(`Invalid number format - ${numberFormat}`);
|
||||
}
|
||||
|
||||
if (!isValidFormat) {
|
||||
errors.push(`Value ${value} does not match the number format - ${numberFormat}`);
|
||||
if (foundRegex && !foundRegex.test(value)) {
|
||||
errors.push(`Value ${value} does not match the number format - ${numberFormat}`);
|
||||
}
|
||||
}
|
||||
|
||||
const numberValue = parseFloat(value);
|
||||
@ -32,19 +33,19 @@ export const validateNumberField = (
|
||||
errors.push(`Value is not a valid number`);
|
||||
}
|
||||
|
||||
if (minValue !== undefined && minValue > 0 && numberValue < minValue) {
|
||||
if (typeof minValue === 'number' && minValue > 0 && numberValue < minValue) {
|
||||
errors.push(`Value ${value} is less than the minimum value of ${minValue}`);
|
||||
}
|
||||
|
||||
if (maxValue !== undefined && maxValue > 0 && numberValue > maxValue) {
|
||||
if (typeof maxValue === 'number' && maxValue > 0 && numberValue > maxValue) {
|
||||
errors.push(`Value ${value} is greater than the maximum value of ${maxValue}`);
|
||||
}
|
||||
|
||||
if (minValue !== undefined && maxValue !== undefined && minValue > maxValue) {
|
||||
if (typeof minValue === 'number' && typeof maxValue === 'number' && minValue > maxValue) {
|
||||
errors.push('Minimum value cannot be greater than maximum value');
|
||||
}
|
||||
|
||||
if (maxValue !== undefined && minValue !== undefined && maxValue < minValue) {
|
||||
if (typeof maxValue === 'number' && typeof minValue === 'number' && maxValue < minValue) {
|
||||
errors.push('Maximum value cannot be less than minimum value');
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema';
|
||||
import type { TShareDocumentRequest } from '@documenso/trpc/server/document-router/share-document.types';
|
||||
|
||||
import { useCopyToClipboard } from './use-copy-to-clipboard';
|
||||
|
||||
@ -12,14 +12,14 @@ export function useCopyShareLink({ onSuccess, onError }: UseCopyShareLinkOptions
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const { mutateAsync: createOrGetShareLink, isPending: isCreatingShareLink } =
|
||||
trpc.shareLink.createOrGetShareLink.useMutation();
|
||||
trpc.document.share.useMutation();
|
||||
|
||||
/**
|
||||
* Copy a newly created, or pre-existing share link to the user's clipboard.
|
||||
*
|
||||
* @param payload The payload to create or get a share link.
|
||||
*/
|
||||
const createAndCopyShareLink = async (payload: TCreateOrGetShareLinkMutationSchema) => {
|
||||
const createAndCopyShareLink = async (payload: TShareDocumentRequest) => {
|
||||
const valueToCopy = createOrGetShareLink(payload).then(
|
||||
(result) => `${window.location.origin}/share/${result.slug}`,
|
||||
);
|
||||
|
||||
281
packages/lib/client-only/hooks/use-editor-fields.ts
Normal file
281
packages/lib/client-only/hooks/use-editor-fields.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
|
||||
import type { TEnvelope } from '../../types/envelope';
|
||||
|
||||
export const ZLocalFieldSchema = z.object({
|
||||
// This is the actual ID of the field if created.
|
||||
id: z.number().optional(),
|
||||
// This is the local client side ID of the field.
|
||||
formId: z.string().min(1),
|
||||
// This is the ID of the envelope item to put the field on.
|
||||
envelopeItemId: z.string(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
recipientId: z.number(),
|
||||
page: z.number().min(1),
|
||||
positionX: z.number().min(0),
|
||||
positionY: z.number().min(0),
|
||||
width: z.number().min(0),
|
||||
height: z.number().min(0),
|
||||
fieldMeta: ZFieldMetaSchema,
|
||||
});
|
||||
|
||||
export type TLocalField = z.infer<typeof ZLocalFieldSchema>;
|
||||
|
||||
const ZEditorFieldsFormSchema = z.object({
|
||||
fields: z.array(ZLocalFieldSchema),
|
||||
});
|
||||
|
||||
export type TEditorFieldsFormSchema = z.infer<typeof ZEditorFieldsFormSchema>;
|
||||
|
||||
type EditorFieldsProps = {
|
||||
envelope: TEnvelope;
|
||||
handleFieldsUpdate: (fields: TLocalField[]) => unknown;
|
||||
};
|
||||
|
||||
type UseEditorFieldsResponse = {
|
||||
localFields: TLocalField[];
|
||||
|
||||
// Selected field
|
||||
selectedField: TLocalField | undefined;
|
||||
setSelectedField: (formId: string | null) => void;
|
||||
|
||||
// Field operations
|
||||
addField: (field: Omit<TLocalField, 'formId'>) => TLocalField;
|
||||
removeFieldsByFormId: (formIds: string[]) => void;
|
||||
updateFieldByFormId: (formId: string, updates: Partial<TLocalField>) => void;
|
||||
duplicateField: (field: TLocalField, recipientId?: number) => TLocalField;
|
||||
duplicateFieldToAllPages: (field: TLocalField, recipientId?: number) => TLocalField[];
|
||||
|
||||
// Field utilities
|
||||
getFieldByFormId: (formId: string) => TLocalField | undefined;
|
||||
getFieldsByRecipient: (recipientId: number) => TLocalField[];
|
||||
|
||||
// Selected recipient
|
||||
selectedRecipient: Recipient | null;
|
||||
setSelectedRecipient: (recipientId: number | null) => void;
|
||||
};
|
||||
|
||||
export const useEditorFields = ({
|
||||
envelope,
|
||||
handleFieldsUpdate,
|
||||
}: EditorFieldsProps): UseEditorFieldsResponse => {
|
||||
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
|
||||
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
|
||||
|
||||
const form = useForm<TEditorFieldsFormSchema>({
|
||||
defaultValues: {
|
||||
fields: envelope.fields.map(
|
||||
(field): TLocalField => ({
|
||||
id: field.id,
|
||||
formId: nanoid(),
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
page: field.page,
|
||||
type: field.type,
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
recipientId: field.recipientId,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
}),
|
||||
),
|
||||
},
|
||||
resolver: zodResolver(ZEditorFieldsFormSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
append,
|
||||
remove,
|
||||
update,
|
||||
fields: localFields,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'fields',
|
||||
keyName: 'react-hook-form-id',
|
||||
});
|
||||
|
||||
const triggerFieldsUpdate = () => {
|
||||
void handleFieldsUpdate(form.getValues().fields);
|
||||
};
|
||||
|
||||
const setSelectedField = (formId: string | null, bypassCheck = false) => {
|
||||
if (!formId) {
|
||||
setSelectedFieldFormId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const foundField = localFields.find((field) => field.formId === formId);
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === foundField?.recipientId,
|
||||
);
|
||||
|
||||
if (recipient) {
|
||||
setSelectedRecipient(recipient.id);
|
||||
}
|
||||
|
||||
if (bypassCheck) {
|
||||
console.log(3);
|
||||
setSelectedFieldFormId(formId);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFieldFormId(foundField?.formId ?? null);
|
||||
};
|
||||
|
||||
const addField = useCallback(
|
||||
(fieldData: Omit<TLocalField, 'formId'>): TLocalField => {
|
||||
const field: TLocalField = {
|
||||
...fieldData,
|
||||
formId: nanoid(12),
|
||||
};
|
||||
|
||||
append(field);
|
||||
triggerFieldsUpdate();
|
||||
setSelectedField(field.formId, true);
|
||||
return field;
|
||||
},
|
||||
[append, triggerFieldsUpdate, setSelectedField],
|
||||
);
|
||||
|
||||
const removeFieldsByFormId = useCallback(
|
||||
(formIds: string[]) => {
|
||||
const indexes = formIds
|
||||
.map((formId) => localFields.findIndex((field) => field.formId === formId))
|
||||
.filter((index) => index !== -1);
|
||||
|
||||
if (indexes.length > 0) {
|
||||
remove(indexes);
|
||||
triggerFieldsUpdate();
|
||||
}
|
||||
},
|
||||
[localFields, remove, triggerFieldsUpdate],
|
||||
);
|
||||
|
||||
const updateFieldByFormId = useCallback(
|
||||
(formId: string, updates: Partial<TLocalField>) => {
|
||||
const index = localFields.findIndex((field) => field.formId === formId);
|
||||
|
||||
if (index !== -1) {
|
||||
update(index, { ...localFields[index], ...updates });
|
||||
triggerFieldsUpdate();
|
||||
}
|
||||
},
|
||||
[localFields, update, triggerFieldsUpdate],
|
||||
);
|
||||
|
||||
const duplicateField = useCallback(
|
||||
(field: TLocalField): TLocalField => {
|
||||
const newField: TLocalField = {
|
||||
...structuredClone(field),
|
||||
id: undefined,
|
||||
formId: nanoid(12),
|
||||
recipientId: field.recipientId,
|
||||
positionX: field.positionX + 3,
|
||||
positionY: field.positionY + 3,
|
||||
};
|
||||
|
||||
append(newField);
|
||||
triggerFieldsUpdate();
|
||||
return newField;
|
||||
},
|
||||
[append, triggerFieldsUpdate],
|
||||
);
|
||||
|
||||
const duplicateFieldToAllPages = useCallback(
|
||||
(field: TLocalField): TLocalField[] => {
|
||||
const pages = Array.from(document.querySelectorAll('[data-page-number]'));
|
||||
const newFields: TLocalField[] = [];
|
||||
|
||||
pages.forEach((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
|
||||
if (pageNumber === field.page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newField: TLocalField = {
|
||||
...structuredClone(field),
|
||||
id: undefined,
|
||||
formId: nanoid(12),
|
||||
page: pageNumber,
|
||||
};
|
||||
|
||||
append(newField);
|
||||
newFields.push(newField);
|
||||
});
|
||||
|
||||
triggerFieldsUpdate();
|
||||
return newFields;
|
||||
},
|
||||
[append, triggerFieldsUpdate],
|
||||
);
|
||||
|
||||
const getFieldByFormId = useCallback(
|
||||
(formId: string): TLocalField | undefined => {
|
||||
return localFields.find((field) => field.formId === formId) as TLocalField | undefined;
|
||||
},
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const getFieldsByRecipient = useCallback(
|
||||
(recipientId: number): TLocalField[] => {
|
||||
return localFields.filter((field) => field.recipientId === recipientId);
|
||||
},
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const selectedRecipient = useMemo(() => {
|
||||
return envelope.recipients.find((recipient) => recipient.id === selectedRecipientId) || null;
|
||||
}, [selectedRecipientId, envelope.recipients]);
|
||||
|
||||
const selectedField = useMemo(() => {
|
||||
return localFields.find((field) => field.formId === selectedFieldFormId);
|
||||
}, [selectedFieldFormId, localFields]);
|
||||
|
||||
/**
|
||||
* Keep the selected field form ID in sync with the local fields.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const foundField = localFields.find((field) => field.formId === selectedFieldFormId);
|
||||
setSelectedFieldFormId(foundField?.formId ?? null);
|
||||
}, [selectedFieldFormId, localFields]);
|
||||
|
||||
const setSelectedRecipient = (recipientId: number | null) => {
|
||||
const foundRecipient = envelope.recipients.find((recipient) => recipient.id === recipientId);
|
||||
|
||||
setSelectedRecipientId(foundRecipient?.id ?? null);
|
||||
};
|
||||
|
||||
return {
|
||||
// Core state
|
||||
localFields,
|
||||
|
||||
// Field operations
|
||||
addField,
|
||||
removeFieldsByFormId,
|
||||
updateFieldByFormId,
|
||||
duplicateField,
|
||||
duplicateFieldToAllPages,
|
||||
|
||||
// Field utilities
|
||||
getFieldByFormId,
|
||||
getFieldsByRecipient,
|
||||
|
||||
// Selected field
|
||||
selectedField,
|
||||
setSelectedField,
|
||||
|
||||
// Selected recipient
|
||||
selectedRecipient,
|
||||
setSelectedRecipient,
|
||||
};
|
||||
};
|
||||
90
packages/lib/client-only/hooks/use-envelope-autosave.ts
Normal file
90
packages/lib/client-only/hooks/use-envelope-autosave.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function useEnvelopeAutosave<T>(saveFn: (data: T) => Promise<void>, delay = 1000) {
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastArgsRef = useRef<T | null>(null);
|
||||
const pendingPromiseRef = useRef<Promise<void> | null>(null);
|
||||
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [isCommiting, setIsCommiting] = useState(false);
|
||||
|
||||
const triggerSave = useCallback(
|
||||
(data: T) => {
|
||||
lastArgsRef.current = data;
|
||||
|
||||
// A debounce or promise means something is pending
|
||||
setIsPending(true);
|
||||
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
timeoutRef.current = setTimeout(async () => {
|
||||
if (!lastArgsRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const args = lastArgsRef.current;
|
||||
lastArgsRef.current = null;
|
||||
timeoutRef.current = null;
|
||||
|
||||
setIsCommiting(true);
|
||||
pendingPromiseRef.current = saveFn(args);
|
||||
|
||||
try {
|
||||
await pendingPromiseRef.current;
|
||||
} finally {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pendingPromiseRef.current = null;
|
||||
setIsCommiting(false);
|
||||
setIsPending(false);
|
||||
}
|
||||
}, delay);
|
||||
},
|
||||
[saveFn, delay],
|
||||
);
|
||||
|
||||
const flush = useCallback(async () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (pendingPromiseRef.current) {
|
||||
// Already running → wait for it
|
||||
await pendingPromiseRef.current;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastArgsRef.current) {
|
||||
const args = lastArgsRef.current;
|
||||
lastArgsRef.current = null;
|
||||
|
||||
setIsCommiting(true);
|
||||
setIsPending(true);
|
||||
|
||||
pendingPromiseRef.current = saveFn(args);
|
||||
try {
|
||||
await pendingPromiseRef.current;
|
||||
} finally {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pendingPromiseRef.current = null;
|
||||
setIsCommiting(false);
|
||||
setIsPending(false);
|
||||
}
|
||||
}
|
||||
}, [saveFn]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = () => {
|
||||
if (timeoutRef.current || pendingPromiseRef.current) {
|
||||
void flush();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}, [flush]);
|
||||
|
||||
return { triggerSave, flush, isPending, isCommiting };
|
||||
}
|
||||
@ -5,7 +5,9 @@ import type { Field } from '@prisma/client';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
|
||||
export const useFieldPageCoords = (field: Field) => {
|
||||
export const useFieldPageCoords = (
|
||||
field: Pick<Field, 'positionX' | 'positionY' | 'width' | 'height' | 'page'>,
|
||||
) => {
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
||||
286
packages/lib/client-only/providers/envelope-editor-provider.tsx
Normal file
286
packages/lib/client-only/providers/envelope-editor-provider.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TSetEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/set-envelope-recipients.types';
|
||||
import type { RecipientColorStyles, TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import {
|
||||
AVAILABLE_RECIPIENT_COLORS,
|
||||
getRecipientColorStyles,
|
||||
} from '@documenso/ui/lib/recipient-colors';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { TEnvelope } from '../../types/envelope';
|
||||
import { useEditorFields } from '../hooks/use-editor-fields';
|
||||
import type { TLocalField } from '../hooks/use-editor-fields';
|
||||
import { useEnvelopeAutosave } from '../hooks/use-envelope-autosave';
|
||||
|
||||
export const useDebounceFunction = <Args extends unknown[]>(
|
||||
callback: (...args: Args) => void,
|
||||
delay: number,
|
||||
) => {
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
return useCallback(
|
||||
(...args: Args) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
callback(...args);
|
||||
}, delay);
|
||||
},
|
||||
[callback, delay],
|
||||
);
|
||||
};
|
||||
|
||||
type EnvelopeEditorProviderValue = {
|
||||
envelope: TEnvelope;
|
||||
isDocument: boolean;
|
||||
isTemplate: boolean;
|
||||
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
|
||||
|
||||
updateEnvelope: (envelopeUpdates: Partial<TEnvelope>) => void;
|
||||
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
|
||||
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
|
||||
|
||||
getFieldColor: (field: TLocalField) => RecipientColorStyles;
|
||||
getRecipientColorKey: (recipientId: number) => TRecipientColor;
|
||||
|
||||
editorFields: ReturnType<typeof useEditorFields>;
|
||||
|
||||
isAutosaving: boolean;
|
||||
flushAutosave: () => void;
|
||||
autosaveError: boolean;
|
||||
|
||||
// refetchEnvelope: () => Promise<void>;
|
||||
// updateEnvelope: (envelope: TEnvelope) => Promise<void>;
|
||||
};
|
||||
|
||||
interface EnvelopeEditorProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialEnvelope: TEnvelope;
|
||||
}
|
||||
|
||||
const EnvelopeEditorContext = createContext<EnvelopeEditorProviderValue | null>(null);
|
||||
|
||||
export const useCurrentEnvelopeEditor = () => {
|
||||
const context = useContext(EnvelopeEditorContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCurrentEnvelopeEditor must be used within a EnvelopeEditorProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const EnvelopeEditorProvider = ({
|
||||
children,
|
||||
initialEnvelope,
|
||||
}: EnvelopeEditorProviderProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [envelope, setEnvelope] = useState(initialEnvelope);
|
||||
|
||||
const [autosaveError, setAutosaveError] = useState<boolean>(false);
|
||||
|
||||
const envelopeUpdateMutationQuery = trpc.envelope.update.useMutation({
|
||||
onSuccess: (response, input) => {
|
||||
console.log(input.meta?.emailSettings);
|
||||
setEnvelope({
|
||||
...envelope,
|
||||
...response,
|
||||
documentMeta: {
|
||||
...envelope.documentMeta,
|
||||
...input.meta,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
emailSettings: (input.meta?.emailSettings ||
|
||||
null) as unknown as TDocumentEmailSettings | null,
|
||||
},
|
||||
});
|
||||
|
||||
setAutosaveError(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setAutosaveError(true);
|
||||
|
||||
toast({
|
||||
title: t`Save failed`,
|
||||
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const envelopeFieldSetMutationQuery = trpc.envelope.field.set.useMutation({
|
||||
onSuccess: () => {
|
||||
setAutosaveError(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setAutosaveError(true);
|
||||
|
||||
toast({
|
||||
title: t`Save failed`,
|
||||
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const envelopeRecipientSetMutationQuery = trpc.envelope.recipient.set.useMutation({
|
||||
onSuccess: () => {
|
||||
setAutosaveError(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setAutosaveError(true);
|
||||
|
||||
toast({
|
||||
title: t`Save failed`,
|
||||
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
triggerSave: setRecipientsDebounced,
|
||||
flush: setRecipientsAsync,
|
||||
isPending: isRecipientsMutationPending,
|
||||
} = useEnvelopeAutosave(async (recipients: TSetEnvelopeRecipientsRequest['recipients']) => {
|
||||
await envelopeRecipientSetMutationQuery.mutateAsync({
|
||||
envelopeId: envelope.id,
|
||||
envelopeType: envelope.type,
|
||||
recipients,
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const {
|
||||
triggerSave: setFieldsDebounced,
|
||||
flush: setFieldsAsync,
|
||||
isPending: isFieldsMutationPending,
|
||||
} = useEnvelopeAutosave(async (fields: TLocalField[]) => {
|
||||
await envelopeFieldSetMutationQuery.mutateAsync({
|
||||
envelopeId: envelope.id,
|
||||
envelopeType: envelope.type,
|
||||
fields,
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const {
|
||||
triggerSave: setEnvelopeDebounced,
|
||||
flush: setEnvelopeAsync,
|
||||
isPending: isEnvelopeMutationPending,
|
||||
} = useEnvelopeAutosave(async (envelopeUpdates: Partial<TEnvelope>) => {
|
||||
await envelopeUpdateMutationQuery.mutateAsync({
|
||||
envelopeId: envelope.id,
|
||||
envelopeType: envelope.type,
|
||||
data: {
|
||||
...envelopeUpdates,
|
||||
},
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* Updates the local envelope and debounces the update to the server.
|
||||
*/
|
||||
const updateEnvelope = (envelopeUpdates: Partial<TEnvelope>) => {
|
||||
setEnvelope((prev) => ({ ...prev, ...envelopeUpdates }));
|
||||
setEnvelopeDebounced(envelopeUpdates);
|
||||
};
|
||||
|
||||
const editorFields = useEditorFields({
|
||||
envelope,
|
||||
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
|
||||
});
|
||||
|
||||
const getFieldColor = useCallback(
|
||||
(field: TLocalField) => {
|
||||
// Todo: Envelopes - Local recipients
|
||||
const recipientIndex = envelope.recipients.findIndex(
|
||||
(recipient) => recipient.id === field.recipientId,
|
||||
);
|
||||
|
||||
return getRecipientColorStyles(Math.max(recipientIndex, 0));
|
||||
},
|
||||
[envelope.recipients], // Todo: Envelopes - Local recipients
|
||||
);
|
||||
|
||||
const getRecipientColorKey = useCallback(
|
||||
(recipientId: number) => {
|
||||
// Todo: Envelopes - Local recipients
|
||||
const recipientIndex = envelope.recipients.findIndex(
|
||||
(recipient) => recipient.id === recipientId,
|
||||
);
|
||||
|
||||
return AVAILABLE_RECIPIENT_COLORS[Math.max(recipientIndex, 0)];
|
||||
},
|
||||
[envelope.recipients], // Todo: Envelopes - Local recipients
|
||||
);
|
||||
|
||||
const { refetch: reloadEnvelope, isLoading: isReloadingEnvelope } = trpc.envelope.get.useQuery(
|
||||
{
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
{
|
||||
initialData: envelope,
|
||||
},
|
||||
);
|
||||
|
||||
const setLocalEnvelope = (localEnvelope: Partial<TEnvelope>) => {
|
||||
setEnvelope((prev) => ({ ...prev, ...localEnvelope }));
|
||||
};
|
||||
|
||||
const isAutosaving = useMemo(() => {
|
||||
return (
|
||||
envelopeFieldSetMutationQuery.isPending ||
|
||||
envelopeRecipientSetMutationQuery.isPending ||
|
||||
envelopeUpdateMutationQuery.isPending ||
|
||||
isFieldsMutationPending ||
|
||||
isRecipientsMutationPending ||
|
||||
isEnvelopeMutationPending
|
||||
);
|
||||
}, [
|
||||
envelopeFieldSetMutationQuery.isPending,
|
||||
envelopeRecipientSetMutationQuery.isPending,
|
||||
envelopeUpdateMutationQuery.isPending,
|
||||
isFieldsMutationPending,
|
||||
isRecipientsMutationPending,
|
||||
isEnvelopeMutationPending,
|
||||
]);
|
||||
|
||||
const flushAutosave = () => {
|
||||
void setFieldsAsync();
|
||||
void setRecipientsAsync();
|
||||
void setEnvelopeAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<EnvelopeEditorContext.Provider
|
||||
value={{
|
||||
envelope,
|
||||
isDocument: envelope.type === EnvelopeType.DOCUMENT,
|
||||
isTemplate: envelope.type === EnvelopeType.TEMPLATE,
|
||||
setLocalEnvelope,
|
||||
getFieldColor,
|
||||
getRecipientColorKey,
|
||||
updateEnvelope,
|
||||
setRecipientsDebounced,
|
||||
setRecipientsAsync,
|
||||
editorFields,
|
||||
autosaveError,
|
||||
flushAutosave,
|
||||
isAutosaving,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</EnvelopeEditorContext.Provider>
|
||||
);
|
||||
};
|
||||
148
packages/lib/client-only/providers/envelope-render-provider.tsx
Normal file
148
packages/lib/client-only/providers/envelope-render-provider.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
|
||||
import type { TEnvelope } from '../../types/envelope';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
|
||||
type FileData =
|
||||
| {
|
||||
status: 'loading' | 'error';
|
||||
}
|
||||
| {
|
||||
file: Uint8Array;
|
||||
status: 'loaded';
|
||||
};
|
||||
|
||||
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
||||
|
||||
type EnvelopeRenderProviderValue = {
|
||||
getPdfBuffer: (documentDataId: string) => FileData | null;
|
||||
envelopeItems: EnvelopeRenderItem[];
|
||||
currentEnvelopeItem: EnvelopeRenderItem | null;
|
||||
setCurrentEnvelopeItem: (envelopeItemId: string) => void;
|
||||
fields: TEnvelope['fields'];
|
||||
};
|
||||
|
||||
interface EnvelopeRenderProviderProps {
|
||||
children: React.ReactNode;
|
||||
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
||||
|
||||
/**
|
||||
* Optional fields which are passed down to renderers for custom rendering needs.
|
||||
*
|
||||
* Only pass if the CustomRenderer you are passing in wants fields.
|
||||
*/
|
||||
fields?: TEnvelope['fields'];
|
||||
}
|
||||
|
||||
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
|
||||
|
||||
export const useCurrentEnvelopeRender = () => {
|
||||
const context = useContext(EnvelopeRenderContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCurrentEnvelopeRender must be used within a EnvelopeRenderProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages fetching and storing PDF files to render on the client.
|
||||
*/
|
||||
export const EnvelopeRenderProvider = ({
|
||||
children,
|
||||
envelope,
|
||||
fields,
|
||||
}: EnvelopeRenderProviderProps) => {
|
||||
// Indexed by documentDataId.
|
||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
||||
|
||||
const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null);
|
||||
|
||||
const envelopeItems = useMemo(
|
||||
() => envelope.envelopeItems.sort((a, b) => a.order - b.order),
|
||||
[envelope.envelopeItems],
|
||||
);
|
||||
|
||||
const loadEnvelopeItemPdfFile = async (documentData: DocumentData) => {
|
||||
if (files[documentData.id]?.status === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!files[documentData.id]) {
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
status: 'loading',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await getFile(documentData);
|
||||
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
file,
|
||||
status: 'loaded',
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const getPdfBuffer = useCallback(
|
||||
(documentDataId: string) => {
|
||||
return files[documentDataId] || null;
|
||||
},
|
||||
[files],
|
||||
);
|
||||
|
||||
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
|
||||
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
|
||||
|
||||
setItem(foundItem ?? null);
|
||||
};
|
||||
|
||||
// Set the selected item to the first item if none is set.
|
||||
useEffect(() => {
|
||||
if (!currentItem && envelopeItems.length > 0) {
|
||||
setCurrentEnvelopeItem(envelopeItems[0].id);
|
||||
}
|
||||
}, [currentItem, envelopeItems]);
|
||||
|
||||
// Look for any missing pdf files and load them.
|
||||
useEffect(() => {
|
||||
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]);
|
||||
|
||||
for (const item of missingFiles) {
|
||||
void loadEnvelopeItemPdfFile(item.documentData);
|
||||
}
|
||||
}, [envelope.envelopeItems]);
|
||||
|
||||
return (
|
||||
<EnvelopeRenderContext.Provider
|
||||
value={{
|
||||
getPdfBuffer,
|
||||
envelopeItems,
|
||||
currentEnvelopeItem: currentItem,
|
||||
setCurrentEnvelopeItem,
|
||||
fields: fields ?? [],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</EnvelopeRenderContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentVisibility, OrganisationGroupType, TeamMemberRole } from '@prisma/client';
|
||||
|
||||
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$');
|
||||
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
|
||||
@ -33,6 +33,16 @@ export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = {
|
||||
MANAGE_TEAM: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||
} satisfies Record<string, TeamMemberRole[]>;
|
||||
|
||||
export const TEAM_DOCUMENT_VISIBILITY_MAP = {
|
||||
[TeamMemberRole.ADMIN]: [
|
||||
DocumentVisibility.ADMIN,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.EVERYONE,
|
||||
],
|
||||
[TeamMemberRole.MANAGER]: [DocumentVisibility.MANAGER_AND_ABOVE, DocumentVisibility.EVERYONE],
|
||||
[TeamMemberRole.MEMBER]: [DocumentVisibility.EVERYONE],
|
||||
} satisfies Record<TeamMemberRole, DocumentVisibility[]>;
|
||||
|
||||
/**
|
||||
* A hierarchy of team member roles to determine which role has higher permission than another.
|
||||
*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
@ -11,6 +11,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendDocumentCancelledEmailsJobDefinition } from './send-document-cancelled-emails';
|
||||
@ -24,10 +25,14 @@ export const run = async ({
|
||||
}) => {
|
||||
const { documentId, cancellationReason } = payload;
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
@ -52,12 +57,12 @@ export const run = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { documentMeta, user: documentOwner } = document;
|
||||
const { documentMeta, user: documentOwner } = envelope;
|
||||
|
||||
// Check if document cancellation emails are enabled
|
||||
const isEmailEnabled = extractDerivedDocumentEmailSettings(documentMeta).documentDeleted;
|
||||
@ -69,7 +74,7 @@ export const run = async ({
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
// Send cancellation emails to all recipients who have been sent the document or viewed it
|
||||
const recipientsToNotify = document.recipients.filter(
|
||||
const recipientsToNotify = envelope.recipients.filter(
|
||||
(recipient) =>
|
||||
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
||||
@ -79,7 +84,7 @@ export const run = async ({
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: documentOwner.name || undefined,
|
||||
inviterEmail: documentOwner.email,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
@ -102,7 +107,7 @@ export const run = async ({
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document "${document.title}" Cancelled`),
|
||||
subject: i18n._(msg`Document "${envelope.title}" Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed';
|
||||
@ -10,6 +11,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||
@ -23,9 +25,15 @@ export const run = async ({
|
||||
}) => {
|
||||
const { documentId, recipientId } = payload;
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
@ -49,25 +57,25 @@ export const run = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigned;
|
||||
|
||||
if (!isRecipientSignedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const [recipient] = envelope.recipients;
|
||||
const { email: recipientEmail, name: recipientName } = recipient;
|
||||
const { user: owner } = document;
|
||||
const { user: owner } = envelope;
|
||||
|
||||
const recipientReference = recipientName || recipientEmail;
|
||||
|
||||
@ -80,9 +88,9 @@ export const run = async ({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
@ -90,7 +98,7 @@ export const run = async ({
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
const template = createElement(DocumentRecipientSignedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
assetBaseUrl,
|
||||
@ -112,7 +120,7 @@ export const run = async ({
|
||||
address: owner.email,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`${recipientReference} has signed "${document.title}"`),
|
||||
subject: i18n._(msg`${recipientReference} has signed "${envelope.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||
@ -13,6 +13,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { DOCUMENSO_INTERNAL_EMAIL } from '../../../constants/email';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../../utils/teams';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
@ -27,11 +28,15 @@ export const run = async ({
|
||||
}) => {
|
||||
const { documentId, recipientId } = payload;
|
||||
|
||||
const [document, recipient] = await Promise.all([
|
||||
prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
const [envelope, recipient] = await Promise.all([
|
||||
prisma.envelope.findFirstOrThrow({
|
||||
where: unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
@ -58,10 +63,10 @@ export const run = async ({
|
||||
}),
|
||||
]);
|
||||
|
||||
const { user: documentOwner } = document;
|
||||
const { user: documentOwner } = envelope;
|
||||
|
||||
const isEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isEmailEnabled) {
|
||||
@ -72,9 +77,9 @@ export const run = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
@ -83,8 +88,8 @@ export const run = async ({
|
||||
await io.runTask('send-rejection-confirmation-email', async () => {
|
||||
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
||||
recipientName: recipient.name,
|
||||
documentName: document.title,
|
||||
documentOwnerName: document.user.name || document.user.email,
|
||||
documentName: envelope.title,
|
||||
documentOwnerName: envelope.user.name || envelope.user.email,
|
||||
reason: recipient.rejectionReason || '',
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
@ -105,7 +110,7 @@ export const run = async ({
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`),
|
||||
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
@ -115,9 +120,9 @@ export const run = async ({
|
||||
await io.runTask('send-owner-notification-email', async () => {
|
||||
const ownerTemplate = createElement(DocumentRejectedEmail, {
|
||||
recipientName: recipient.name,
|
||||
documentName: document.title,
|
||||
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
|
||||
document.id
|
||||
documentName: envelope.title,
|
||||
documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(envelope.team?.url)}/${
|
||||
envelope.id
|
||||
}`,
|
||||
rejectionReason: recipient.rejectionReason || '',
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
@ -138,7 +143,7 @@ export const run = async ({
|
||||
address: documentOwner.email,
|
||||
},
|
||||
from: DOCUMENSO_INTERNAL_EMAIL, // Purposefully using internal email here.
|
||||
subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`),
|
||||
subject: i18n._(msg`Document "${envelope.title}" - Rejected by ${recipient.name}`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
@ -23,6 +24,7 @@ import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
@ -37,7 +39,7 @@ export const run = async ({
|
||||
}) => {
|
||||
const { userId, documentId, recipientId, requestMetadata } = payload;
|
||||
|
||||
const [user, document, recipient] = await Promise.all([
|
||||
const [user, envelope, recipient] = await Promise.all([
|
||||
prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
@ -48,9 +50,15 @@ export const run = async ({
|
||||
name: true,
|
||||
},
|
||||
}),
|
||||
prisma.document.findFirstOrThrow({
|
||||
prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
status: DocumentStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
@ -70,14 +78,14 @@ export const run = async ({
|
||||
}),
|
||||
]);
|
||||
|
||||
const { documentMeta, team } = document;
|
||||
const { documentMeta, team } = envelope;
|
||||
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
@ -89,13 +97,13 @@ export const run = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
const customEmail = envelope?.documentMeta;
|
||||
const isDirectTemplate = envelope.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
@ -113,7 +121,7 @@ export const run = async ({
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
@ -136,8 +144,8 @@ export const run = async ({
|
||||
|
||||
emailMessage = i18n._(
|
||||
settings.includeSenderDetails
|
||||
? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`
|
||||
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".`
|
||||
: msg`${team.name} has invited you to ${recipientActionVerb} the document "${envelope.title}".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -145,14 +153,14 @@ export const run = async ({
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': document.title,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
@ -210,7 +218,7 @@ export const run = async ({
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user,
|
||||
requestMetadata,
|
||||
data: {
|
||||
|
||||
@ -37,7 +37,10 @@ export const run = async ({
|
||||
const { userId, teamId, templateId, csvContent, sendImmediately, requestMetadata } = payload;
|
||||
|
||||
const template = await getTemplateById({
|
||||
id: templateId,
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
},
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
@ -99,9 +102,12 @@ export const run = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const document = await io.runTask(`create-document-${rowIndex}`, async () => {
|
||||
const envelope = await io.runTask(`create-document-${rowIndex}`, async () => {
|
||||
return await createDocumentFromTemplate({
|
||||
templateId: template.id,
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: template.id,
|
||||
},
|
||||
userId,
|
||||
teamId,
|
||||
recipients: recipients.map((recipient, index) => {
|
||||
@ -124,7 +130,10 @@ export const run = async ({
|
||||
if (sendImmediately) {
|
||||
await io.runTask(`send-document-${rowIndex}`, async () => {
|
||||
await sendDocument({
|
||||
documentId: document.id,
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata: {
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||
import type { DocumentData, DocumentMeta, Envelope, EnvelopeItem, Field } from '@prisma/client';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
@ -14,7 +21,8 @@ import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificat
|
||||
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||
import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
|
||||
import { insertFieldInPDFV1 } from '../../../server-only/pdf/insert-field-in-pdf-v1';
|
||||
import { insertFieldInPDFV2 } from '../../../server-only/pdf/insert-field-in-pdf-v2';
|
||||
import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf';
|
||||
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
|
||||
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
|
||||
@ -22,7 +30,7 @@ import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-we
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../../types/webhook-payload';
|
||||
import { prefixedId } from '../../../universal/id';
|
||||
import { getFileServerSide } from '../../../universal/upload/get-file.server';
|
||||
@ -30,6 +38,7 @@ import { putPdfFileServerSide } from '../../../universal/upload/put-file.server'
|
||||
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
|
||||
import { isDocumentCompleted } from '../../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import { mapDocumentIdToSecondaryId, mapSecondaryIdToDocumentId } from '../../../utils/envelope';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSealDocumentJobDefinition } from './seal-document';
|
||||
|
||||
@ -42,24 +51,39 @@ export const run = async ({
|
||||
}) => {
|
||||
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
secondaryId: mapDocumentIdToSecondaryId(documentId),
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
field: {
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (envelope.envelopeItems.length === 0) {
|
||||
throw new Error('At least one envelope item required');
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
|
||||
const isComplete =
|
||||
document.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
||||
document.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
||||
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
||||
envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
||||
|
||||
if (!isComplete) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
@ -71,28 +95,28 @@ export const run = async ({
|
||||
// after it has already run through the update task further below.
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
const documentStatus = await io.runTask('get-document-status', async () => {
|
||||
return document.status;
|
||||
return envelope.status;
|
||||
});
|
||||
|
||||
// This is the same case as above.
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
const documentDataId = await io.runTask('get-document-data-id', async () => {
|
||||
return document.documentDataId;
|
||||
});
|
||||
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
let envelopeItems = await io.runTask(
|
||||
'get-document-data-id',
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async () => {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
return envelope.envelopeItems.map(({ field, ...rest }) => ({
|
||||
...rest,
|
||||
}));
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!documentData) {
|
||||
throw new Error(`Document ${document.id} has no document data`);
|
||||
if (envelopeItems.length < 1) {
|
||||
throw new Error(`Document ${envelope.id} has no envelope items`);
|
||||
}
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
@ -111,7 +135,7 @@ export const run = async ({
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
@ -120,19 +144,25 @@ export const run = async ({
|
||||
|
||||
// Skip the field check if the document is rejected
|
||||
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Document ${document.id} has unsigned required fields`);
|
||||
throw new Error(`Document ${envelope.id} has unsigned required fields`);
|
||||
}
|
||||
|
||||
if (isResealing) {
|
||||
// If we're resealing we want to use the initial data for the document
|
||||
// so we aren't placing fields on top of eachother.
|
||||
documentData.data = documentData.initialData;
|
||||
envelopeItems = envelopeItems.map((envelopeItem) => ({
|
||||
...envelopeItem,
|
||||
documentData: {
|
||||
...envelopeItem.documentData,
|
||||
data: envelopeItem.documentData.initialData,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (!document.qrToken) {
|
||||
await prisma.document.update({
|
||||
if (!envelope.qrToken) {
|
||||
await prisma.envelope.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
qrToken: prefixedId('qr'),
|
||||
@ -140,97 +170,38 @@ export const run = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const pdfData = await getFileServerSide(documentData);
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
const certificateData = settings.includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get certificate PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||
const pdfDoc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
normalizeSignatureAppearances(pdfDoc);
|
||||
await flattenForm(pdfDoc);
|
||||
flattenAnnotations(pdfDoc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
if (isRejected && rejectionReason) {
|
||||
await addRejectionStampToPdf(pdfDoc, rejectionReason);
|
||||
}
|
||||
|
||||
if (certificateData) {
|
||||
const certificateDoc = await PDFDocument.load(certificateData);
|
||||
|
||||
const certificatePages = await pdfDoc.copyPages(
|
||||
certificateDoc,
|
||||
certificateDoc.getPageIndices(),
|
||||
);
|
||||
|
||||
certificatePages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLogDoc = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await pdfDoc.copyPages(auditLogDoc, auditLogDoc.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.inserted) {
|
||||
document.useLegacyFieldInsertion
|
||||
? await legacy_insertFieldInPDF(pdfDoc, field)
|
||||
: await insertFieldInPDF(pdfDoc, field);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-flatten the form to handle our checkbox and radio fields that
|
||||
// create native arcoFields
|
||||
await flattenForm(pdfDoc);
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name } = path.parse(document.title);
|
||||
|
||||
// Add suffix based on document status
|
||||
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
||||
|
||||
const documentData = await putPdfFileServerSide({
|
||||
name: `${name}${suffix}`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
|
||||
return documentData.id;
|
||||
const { certificateData, auditLogData } = await getCertificateAndAuditLogData({
|
||||
legacyDocumentId,
|
||||
documentMeta: envelope.documentMeta,
|
||||
settings,
|
||||
});
|
||||
|
||||
const newDocumentData = await Promise.all(
|
||||
envelopeItems.map(async (envelopeItem) =>
|
||||
io.runTask('decorate-and-sign-pdf', async () => {
|
||||
const envelopeItemFields = envelope.envelopeItems.find(
|
||||
(item) => item.id === envelopeItem.id,
|
||||
)?.field;
|
||||
|
||||
if (!envelopeItemFields) {
|
||||
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||
}
|
||||
|
||||
return decorateAndSignPdf({
|
||||
envelope,
|
||||
envelopeItem,
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
certificateData,
|
||||
auditLogData,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const postHog = PostHogServerClient();
|
||||
|
||||
if (postHog) {
|
||||
@ -238,7 +209,7 @@ export const run = async ({
|
||||
distinctId: nanoid(),
|
||||
event: 'App: Document Sealed',
|
||||
properties: {
|
||||
documentId: document.id,
|
||||
documentId: envelope.id,
|
||||
isRejected,
|
||||
},
|
||||
});
|
||||
@ -246,15 +217,26 @@ export const run = async ({
|
||||
|
||||
await io.runTask('update-document', async () => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const newData = await tx.documentData.findFirstOrThrow({
|
||||
where: {
|
||||
id: newDataId,
|
||||
},
|
||||
});
|
||||
for (const { oldDocumentDataId, newDocumentDataId } of newDocumentData) {
|
||||
const newData = await tx.documentData.findFirstOrThrow({
|
||||
where: {
|
||||
id: newDocumentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.document.update({
|
||||
await tx.documentData.update({
|
||||
where: {
|
||||
id: oldDocumentDataId,
|
||||
},
|
||||
data: {
|
||||
data: newData.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await tx.envelope.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
||||
@ -262,19 +244,10 @@ export const run = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentData.update({
|
||||
where: {
|
||||
id: documentData.id,
|
||||
},
|
||||
data: {
|
||||
data: newData.data,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
requestMetadata,
|
||||
user: null,
|
||||
data: {
|
||||
@ -289,21 +262,23 @@ export const run = async ({
|
||||
await io.runTask('send-completed-email', async () => {
|
||||
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
|
||||
|
||||
if (isResealing && !isDocumentCompleted(document.status)) {
|
||||
if (isResealing && !isDocumentCompleted(envelope.status)) {
|
||||
shouldSendCompletedEmail = sendEmail;
|
||||
}
|
||||
|
||||
if (shouldSendCompletedEmail) {
|
||||
await sendCompletedEmail({ documentId, requestMetadata });
|
||||
await sendCompletedEmail({
|
||||
id: { type: 'envelopeId', id: envelope.id },
|
||||
requestMetadata,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
@ -313,8 +288,148 @@ export const run = async ({
|
||||
event: isRejected
|
||||
? WebhookTriggerEvents.DOCUMENT_REJECTED
|
||||
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)),
|
||||
userId: updatedEnvelope.userId,
|
||||
teamId: updatedEnvelope.teamId ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
type DecorateAndSignPdfOptions = {
|
||||
envelope: Pick<Envelope, 'id' | 'title' | 'useLegacyFieldInsertion' | 'internalVersion'>;
|
||||
envelopeItem: EnvelopeItem & { documentData: DocumentData };
|
||||
envelopeItemFields: Field[];
|
||||
isRejected: boolean;
|
||||
rejectionReason: string;
|
||||
certificateData: Buffer | null;
|
||||
auditLogData: Buffer | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch, normalize, flatten and insert fields into a PDF document.
|
||||
*/
|
||||
const decorateAndSignPdf = async ({
|
||||
envelope,
|
||||
envelopeItem,
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
certificateData,
|
||||
auditLogData,
|
||||
}: DecorateAndSignPdfOptions) => {
|
||||
const pdfData = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
const pdfDoc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
normalizeSignatureAppearances(pdfDoc);
|
||||
await flattenForm(pdfDoc);
|
||||
flattenAnnotations(pdfDoc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
if (isRejected && rejectionReason) {
|
||||
await addRejectionStampToPdf(pdfDoc, rejectionReason);
|
||||
}
|
||||
|
||||
if (certificateData) {
|
||||
const certificateDoc = await PDFDocument.load(certificateData);
|
||||
|
||||
const certificatePages = await pdfDoc.copyPages(
|
||||
certificateDoc,
|
||||
certificateDoc.getPageIndices(),
|
||||
);
|
||||
|
||||
certificatePages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLogDoc = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await pdfDoc.copyPages(auditLogDoc, auditLogDoc.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of envelopeItemFields) {
|
||||
if (field.inserted) {
|
||||
if (envelope.internalVersion === 2) {
|
||||
await insertFieldInPDFV2(pdfDoc, field);
|
||||
} else if (envelope.useLegacyFieldInsertion) {
|
||||
await legacy_insertFieldInPDF(pdfDoc, field);
|
||||
} else {
|
||||
await insertFieldInPDFV1(pdfDoc, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-flatten the form to handle our checkbox and radio fields that
|
||||
// create native arcoFields
|
||||
await flattenForm(pdfDoc);
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name } = path.parse(envelopeItem.title);
|
||||
|
||||
// Add suffix based on document status
|
||||
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
name: `${name}${suffix}`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
|
||||
return {
|
||||
oldDocumentDataId: envelopeItem.documentData.id,
|
||||
newDocumentDataId: newDocumentData.id,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCertificateAndAuditLogData = async ({
|
||||
legacyDocumentId,
|
||||
documentMeta,
|
||||
settings,
|
||||
}: {
|
||||
legacyDocumentId: number;
|
||||
documentMeta: DocumentMeta;
|
||||
settings: { includeSigningCertificate: boolean; includeAuditLog: boolean };
|
||||
}) => {
|
||||
const getCertificateDataPromise = settings.includeSigningCertificate
|
||||
? getCertificatePdf({
|
||||
documentId: legacyDocumentId,
|
||||
language: documentMeta.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get certificate PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const getAuditLogDataPromise = settings.includeAuditLog
|
||||
? getAuditLogsPdf({
|
||||
documentId: legacyDocumentId,
|
||||
language: documentMeta.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const [certificateData, auditLogData] = await Promise.all([
|
||||
getCertificateDataPromise,
|
||||
getAuditLogDataPromise,
|
||||
]);
|
||||
|
||||
return {
|
||||
certificateData,
|
||||
auditLogData,
|
||||
};
|
||||
};
|
||||
|
||||
@ -43,7 +43,6 @@
|
||||
"micro": "^10.0.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"oslo": "^0.17.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
@ -53,6 +52,7 @@
|
||||
"react": "^18",
|
||||
"remeda": "^2.17.3",
|
||||
"sharp": "0.32.6",
|
||||
"skia-canvas": "^3.0.8",
|
||||
"stripe": "^12.7.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "3.24.1"
|
||||
|
||||
@ -7,7 +7,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '../../../constants/crypto';
|
||||
const ISSUER = 'Documenso Email 2FA';
|
||||
|
||||
export type GenerateTwoFactorCredentialsFromEmailOptions = {
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
@ -18,14 +18,14 @@ export type GenerateTwoFactorCredentialsFromEmailOptions = {
|
||||
* @returns Object containing the token and the 6-digit code
|
||||
*/
|
||||
export const generateTwoFactorCredentialsFromEmail = ({
|
||||
documentId,
|
||||
envelopeId,
|
||||
email,
|
||||
}: GenerateTwoFactorCredentialsFromEmailOptions) => {
|
||||
if (!DOCUMENSO_ENCRYPTION_KEY) {
|
||||
throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
|
||||
}
|
||||
|
||||
const identity = `email-2fa|v1|email:${email}|id:${documentId}`;
|
||||
const identity = `email-2fa|v1|email:${email}|id:${envelopeId}`;
|
||||
|
||||
const secret = hmac(sha256, DOCUMENSO_ENCRYPTION_KEY, identity);
|
||||
|
||||
|
||||
@ -3,17 +3,17 @@ import { generateHOTP } from 'oslo/otp';
|
||||
import { generateTwoFactorCredentialsFromEmail } from './generate-2fa-credentials-from-email';
|
||||
|
||||
export type GenerateTwoFactorTokenFromEmailOptions = {
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
email: string;
|
||||
period?: number;
|
||||
};
|
||||
|
||||
export const generateTwoFactorTokenFromEmail = async ({
|
||||
email,
|
||||
documentId,
|
||||
envelopeId,
|
||||
period = 30_000,
|
||||
}: GenerateTwoFactorTokenFromEmailOptions) => {
|
||||
const { secret } = generateTwoFactorCredentialsFromEmail({ email, documentId });
|
||||
const { secret } = generateTwoFactorCredentialsFromEmail({ email, envelopeId });
|
||||
|
||||
const counter = Math.floor(Date.now() / period);
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
||||
@ -11,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../../email/get-email-context';
|
||||
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from './constants';
|
||||
@ -18,13 +20,19 @@ import { generateTwoFactorTokenFromEmail } from './generate-2fa-token-from-email
|
||||
|
||||
export type Send2FATokenEmailOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
};
|
||||
|
||||
export const send2FATokenEmail = async ({ token, documentId }: Send2FATokenEmailOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
export const send2FATokenEmail = async ({ token, envelopeId }: Send2FATokenEmailOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(
|
||||
{
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
EnvelopeType.DOCUMENT,
|
||||
),
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -47,13 +55,13 @@ export const send2FATokenEmail = async ({ token, documentId }: Send2FATokenEmail
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const [recipient] = envelope.recipients;
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -62,7 +70,7 @@ export const send2FATokenEmail = async ({ token, documentId }: Send2FATokenEmail
|
||||
}
|
||||
|
||||
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
||||
documentId,
|
||||
envelopeId,
|
||||
email: recipient.email,
|
||||
});
|
||||
|
||||
@ -70,9 +78,9 @@ export const send2FATokenEmail = async ({ token, documentId }: Send2FATokenEmail
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
@ -80,7 +88,7 @@ export const send2FATokenEmail = async ({ token, documentId }: Send2FATokenEmail
|
||||
const subject = i18n._(msg`Your two-factor authentication code`);
|
||||
|
||||
const template = createElement(AccessAuth2FAEmailTemplate, {
|
||||
documentTitle: document.title,
|
||||
documentTitle: envelope.title,
|
||||
userName: recipient.name,
|
||||
userEmail: recipient.email,
|
||||
code: twoFactorTokenToken,
|
||||
@ -110,7 +118,7 @@ export const send2FATokenEmail = async ({ token, documentId }: Send2FATokenEmail
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_REQUESTED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
|
||||
@ -3,7 +3,7 @@ import { generateHOTP } from 'oslo/otp';
|
||||
import { generateTwoFactorCredentialsFromEmail } from './generate-2fa-credentials-from-email';
|
||||
|
||||
export type ValidateTwoFactorTokenFromEmailOptions = {
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
email: string;
|
||||
code: string;
|
||||
period?: number;
|
||||
@ -11,13 +11,13 @@ export type ValidateTwoFactorTokenFromEmailOptions = {
|
||||
};
|
||||
|
||||
export const validateTwoFactorTokenFromEmail = async ({
|
||||
documentId,
|
||||
envelopeId,
|
||||
email,
|
||||
code,
|
||||
period = 30_000,
|
||||
window = 1,
|
||||
}: ValidateTwoFactorTokenFromEmailOptions) => {
|
||||
const { secret } = generateTwoFactorCredentialsFromEmail({ email, documentId });
|
||||
const { secret } = generateTwoFactorCredentialsFromEmail({ email, envelopeId });
|
||||
|
||||
let now = Date.now();
|
||||
|
||||
|
||||
98
packages/lib/server-only/admin/admin-find-documents.ts
Normal file
98
packages/lib/server-only/admin/admin-find-documents.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { EnvelopeType, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface AdminFindDocumentsOptions {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
export const adminFindDocuments = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: AdminFindDocumentsOptions) => {
|
||||
let termFilters: Prisma.EnvelopeWhereInput | undefined = !query
|
||||
? undefined
|
||||
: {
|
||||
title: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
};
|
||||
|
||||
if (query && query.startsWith('envelope_')) {
|
||||
termFilters = {
|
||||
id: {
|
||||
equals: query,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (query && query.startsWith('document_')) {
|
||||
termFilters = {
|
||||
secondaryId: {
|
||||
equals: query,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const isQueryAnInteger = !isNaN(parseInt(query));
|
||||
|
||||
if (isQueryAnInteger) {
|
||||
termFilters = {
|
||||
secondaryId: {
|
||||
equals: `document_${query}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
...termFilters,
|
||||
},
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
...termFilters,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -17,15 +17,18 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export type SuperDeleteDocumentOptions = {
|
||||
id: number;
|
||||
export type AdminSuperDeleteDocumentOptions = {
|
||||
envelopeId: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
export const adminSuperDeleteDocument = async ({
|
||||
envelopeId,
|
||||
requestMetadata,
|
||||
}: AdminSuperDeleteDocumentOptions) => {
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: {
|
||||
id,
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
@ -40,7 +43,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
@ -50,38 +53,38 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { status, user } = document;
|
||||
const { status, user } = envelope;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (
|
||||
status === DocumentStatus.PENDING &&
|
||||
document.recipients.length > 0 &&
|
||||
envelope.recipients.length > 0 &&
|
||||
isDocumentDeletedEmailEnabled
|
||||
) {
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
envelope.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = document.documentMeta?.language ?? settings.documentLanguage;
|
||||
const lang = envelope.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
@ -113,7 +116,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: id,
|
||||
envelopeId,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
user,
|
||||
requestMetadata,
|
||||
@ -123,6 +126,6 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.document.delete({ where: { id } });
|
||||
return await tx.envelope.delete({ where: { id: envelopeId } });
|
||||
});
|
||||
};
|
||||
@ -1,58 +0,0 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindDocumentsOptions {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocumentsOptions) => {
|
||||
const termFilters: Prisma.DocumentWhereInput | undefined = !query
|
||||
? undefined
|
||||
: {
|
||||
title: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
};
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.document.findMany({
|
||||
where: {
|
||||
...termFilters,
|
||||
},
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
where: {
|
||||
...termFilters,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -1,8 +1,13 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
export const getDocumentStats = async () => {
|
||||
const counts = await prisma.document.groupBy({
|
||||
const counts = await prisma.envelope.groupBy({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetEntireDocumentOptions = {
|
||||
id: number;
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
|
||||
export type unsafeGetEntireEnvelopeOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
type: EnvelopeType;
|
||||
};
|
||||
|
||||
export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
/**
|
||||
* An unauthenticated function that returns the whole envelope
|
||||
*/
|
||||
export const unsafeGetEntireEnvelope = async ({ id, type }: unsafeGetEntireEnvelopeOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, type),
|
||||
include: {
|
||||
documentMeta: true,
|
||||
user: {
|
||||
@ -30,5 +38,11 @@ export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
return envelope;
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
@ -31,22 +31,23 @@ export async function getSigningVolume({
|
||||
.selectFrom('Subscription as s')
|
||||
.innerJoin('Organisation as o', 's.organisationId', 'o.id')
|
||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
||||
.leftJoin('Document as d', (join) =>
|
||||
.leftJoin('Envelope as e', (join) =>
|
||||
join
|
||||
.onRef('t.id', '=', 'd.teamId')
|
||||
.on('d.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('d.deletedAt', 'is', null),
|
||||
.onRef('t.id', '=', 'e.teamId')
|
||||
.on('e.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('e.deletedAt', 'is', null),
|
||||
)
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
||||
)
|
||||
.where('e.type', '=', EnvelopeType.DOCUMENT)
|
||||
.select([
|
||||
's.id as id',
|
||||
's.createdAt as createdAt',
|
||||
's.planId as planId',
|
||||
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
||||
sql<number>`COUNT(DISTINCT d.id)`.as('signingVolume'),
|
||||
sql<number>`COUNT(DISTINCT e.id)`.as('signingVolume'),
|
||||
])
|
||||
.groupBy(['s.id', 'o.name']);
|
||||
|
||||
|
||||
@ -32,12 +32,13 @@ type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{
|
||||
export const getUserWithSignedDocumentMonthlyGrowth = async () => {
|
||||
const result = await prisma.$queryRaw<GetUserWithDocumentMonthlyGrowthQueryResult>`
|
||||
SELECT
|
||||
DATE_TRUNC('month', "Document"."createdAt") AS "month",
|
||||
COUNT(DISTINCT "Document"."userId") as "count",
|
||||
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count"
|
||||
FROM "Document"
|
||||
INNER JOIN "Team" ON "Document"."teamId" = "Team"."id"
|
||||
DATE_TRUNC('month', "Envelope"."createdAt") AS "month",
|
||||
COUNT(DISTINCT "Envelope"."userId") as "count",
|
||||
COUNT(DISTINCT CASE WHEN "Envelope"."status" = 'COMPLETED' THEN "Envelope"."userId" END) as "signed_count"
|
||||
FROM "Envelope"
|
||||
INNER JOIN "Team" ON "Envelope"."teamId" = "Team"."id"
|
||||
INNER JOIN "Organisation" ON "Team"."organisationId" = "Organisation"."id"
|
||||
WHERE "Envelope"."type" = 'DOCUMENT'::"EnvelopeType"
|
||||
GROUP BY "month"
|
||||
ORDER BY "month" DESC
|
||||
LIMIT 12
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import {
|
||||
type DocumentDistributionMethod,
|
||||
type DocumentSigningOrder,
|
||||
EnvelopeType,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
@ -12,16 +16,16 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type CreateDocumentMetaOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
timezone?: string;
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
emailId?: string | null;
|
||||
@ -39,15 +43,14 @@ export type CreateDocumentMetaOptions = {
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const upsertDocumentMeta = async ({
|
||||
export const updateDocumentMeta = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
dateFormat,
|
||||
documentId,
|
||||
password,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
@ -63,26 +66,27 @@ export const upsertDocumentMeta = async ({
|
||||
expiryUnit,
|
||||
requestMetadata,
|
||||
}: CreateDocumentMetaOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput, team } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null, // Allow updating both documents and templates meta.
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { documentMeta: originalDocumentMeta } = document;
|
||||
const { documentMeta: originalDocumentMeta } = envelope;
|
||||
|
||||
// Validate the emailId belongs to the organisation.
|
||||
if (emailId) {
|
||||
@ -101,35 +105,13 @@ export const upsertDocumentMeta = async ({
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const upsertedDocumentMeta = await tx.documentMeta.upsert({
|
||||
const upsertedDocumentMeta = await tx.documentMeta.update({
|
||||
where: {
|
||||
documentId,
|
||||
id: envelope.documentMetaId,
|
||||
},
|
||||
create: {
|
||||
data: {
|
||||
subject,
|
||||
message,
|
||||
password,
|
||||
dateFormat,
|
||||
timezone,
|
||||
documentId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailId,
|
||||
emailReplyTo,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
language,
|
||||
expiryAmount,
|
||||
expiryUnit,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
message,
|
||||
password,
|
||||
dateFormat,
|
||||
timezone,
|
||||
redirectUrl,
|
||||
@ -157,7 +139,7 @@ export const upsertDocumentMeta = async ({
|
||||
|
||||
await tx.recipient.updateMany({
|
||||
where: {
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
signingStatus: { not: 'SIGNED' },
|
||||
role: { not: 'CC' },
|
||||
},
|
||||
@ -169,11 +151,12 @@ export const upsertDocumentMeta = async ({
|
||||
|
||||
const changes = diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta);
|
||||
|
||||
if (changes.length > 0) {
|
||||
// Create audit logs only for document type envelopes.
|
||||
if (changes.length > 0 && envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@ -22,9 +23,11 @@ import type { TRecipientAccessAuth, TRecipientActionAuth } from '../../types/doc
|
||||
import { DocumentAuth } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
@ -32,7 +35,7 @@ import { sendPendingEmail } from './send-pending-email';
|
||||
|
||||
export type CompleteDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId?: number;
|
||||
authOptions?: TRecipientActionAuth;
|
||||
accessAuthOptions?: TRecipientAccessAuth;
|
||||
@ -43,10 +46,17 @@ export type CompleteDocumentWithTokenOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
id,
|
||||
userId,
|
||||
accessAuthOptions,
|
||||
requestMetadata,
|
||||
nextSigner,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -62,27 +72,18 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
userId,
|
||||
accessAuthOptions,
|
||||
requestMetadata,
|
||||
nextSigner,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
const document = await getDocument({ token, documentId });
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
if (document.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${envelope.id} must be pending`);
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
throw new Error(`Document ${document.id} has no recipient with token ${token}`);
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error(`Document ${envelope.id} has no recipient with token ${token}`);
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const [recipient] = envelope.recipients;
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
@ -95,7 +96,7 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||
|
||||
if (!isRecipientsTurn) {
|
||||
@ -107,7 +108,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
@ -118,7 +119,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
// Check ACCESS AUTH 2FA validation during document completion
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
documentAuth: envelope.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
@ -131,7 +132,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
const isValid = await isRecipientAuthorized({
|
||||
type: 'ACCESS_2FA',
|
||||
documentAuthOptions: document.authOptions,
|
||||
documentAuthOptions: envelope.authOptions,
|
||||
recipient: recipient,
|
||||
userId, // Can be undefined for non-account recipients
|
||||
authOptions: accessAuthOptions,
|
||||
@ -141,7 +142,7 @@ export const completeDocumentWithToken = async ({
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_FAILED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
@ -158,7 +159,7 @@ export const completeDocumentWithToken = async ({
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_VALIDATED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
@ -180,14 +181,14 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
|
||||
const authOptions = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
documentAuth: envelope.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -207,7 +208,7 @@ export const completeDocumentWithToken = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'send.recipient.signed.email',
|
||||
payload: {
|
||||
documentId: document.id,
|
||||
documentId: legacyDocumentId,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
@ -221,7 +222,7 @@ export const completeDocumentWithToken = async ({
|
||||
role: true,
|
||||
},
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
},
|
||||
@ -235,17 +236,17 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
|
||||
if (pendingRecipients.length > 0) {
|
||||
await sendPendingEmail({ documentId, recipientId: recipient.id });
|
||||
await sendPendingEmail({ id, recipientId: recipient.id });
|
||||
|
||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const [nextRecipient] = pendingRecipients;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (nextSigner && document.documentMeta?.allowDictateNextSigner) {
|
||||
if (nextSigner && envelope.documentMeta?.allowDictateNextSigner) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -277,7 +278,7 @@ export const completeDocumentWithToken = async ({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
...(nextSigner && document.documentMeta?.allowDictateNextSigner
|
||||
...(nextSigner && envelope.documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
email: nextSigner.email,
|
||||
@ -289,8 +290,8 @@ export const completeDocumentWithToken = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'send.signing.requested.email',
|
||||
payload: {
|
||||
userId: document.userId,
|
||||
documentId: document.id,
|
||||
userId: envelope.userId,
|
||||
documentId: legacyDocumentId,
|
||||
recipientId: nextRecipient.id,
|
||||
requestMetadata,
|
||||
},
|
||||
@ -299,9 +300,9 @@ export const completeDocumentWithToken = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const haveAllRecipientsSigned = await prisma.document.findFirst({
|
||||
const haveAllRecipientsSigned = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
recipients: {
|
||||
every: {
|
||||
OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }],
|
||||
@ -314,15 +315,16 @@ export const completeDocumentWithToken = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId: document.id,
|
||||
documentId: legacyDocumentId,
|
||||
requestMetadata,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||
const updatedDocument = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@ -332,7 +334,7 @@ export const completeDocumentWithToken = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SIGNED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: updatedDocument.userId,
|
||||
teamId: updatedDocument.teamId ?? undefined,
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Document, DocumentMeta, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
@ -15,18 +15,19 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
@ -50,24 +51,23 @@ export const deleteDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
// Note: This is an unsafe request, we validate the ownership later in the function.
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserTeamMember = await getMemberRoles({
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
@ -76,8 +76,8 @@ export const deleteDocument = async ({
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const isUserOwner = document.userId === userId;
|
||||
const userRecipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||
const isUserOwner = envelope.userId === userId;
|
||||
const userRecipient = envelope.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
@ -88,7 +88,7 @@ export const deleteDocument = async ({
|
||||
// Handle hard or soft deleting the actual document if user has permission.
|
||||
if (isUserOwner || isUserTeamMember) {
|
||||
await handleDocumentOwnerDelete({
|
||||
document,
|
||||
envelope,
|
||||
user,
|
||||
requestMetadata,
|
||||
});
|
||||
@ -113,27 +113,16 @@ export const deleteDocument = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CANCELLED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Return partial document for API v1 response.
|
||||
return {
|
||||
id: document.id,
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
title: document.title,
|
||||
status: document.status,
|
||||
documentDataId: document.documentDataId,
|
||||
createdAt: document.createdAt,
|
||||
updatedAt: document.updatedAt,
|
||||
completedAt: document.completedAt,
|
||||
};
|
||||
return envelope;
|
||||
};
|
||||
|
||||
type HandleDocumentOwnerDeleteOptions = {
|
||||
document: Document & {
|
||||
envelope: Envelope & {
|
||||
recipients: Recipient[];
|
||||
documentMeta: DocumentMeta | null;
|
||||
};
|
||||
@ -142,11 +131,11 @@ type HandleDocumentOwnerDeleteOptions = {
|
||||
};
|
||||
|
||||
const handleDocumentOwnerDelete = async ({
|
||||
document,
|
||||
envelope,
|
||||
user,
|
||||
requestMetadata,
|
||||
}: HandleDocumentOwnerDeleteOptions) => {
|
||||
if (document.deletedAt) {
|
||||
if (envelope.deletedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -154,17 +143,17 @@ const handleDocumentOwnerDelete = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
// Soft delete completed documents.
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
@ -173,9 +162,9 @@ const handleDocumentOwnerDelete = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.document.update({
|
||||
return await tx.envelope.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date().toISOString(),
|
||||
@ -185,12 +174,12 @@ const handleDocumentOwnerDelete = async ({
|
||||
}
|
||||
|
||||
// Hard delete draft and pending documents.
|
||||
const deletedDocument = await prisma.$transaction(async (tx) => {
|
||||
const deletedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
// Currently redundant since deleting a document will delete the audit logs.
|
||||
// However may be useful if we disassociate audit logs and documents if required.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
@ -199,9 +188,9 @@ const handleDocumentOwnerDelete = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.document.delete({
|
||||
return await tx.envelope.delete({
|
||||
where: {
|
||||
id: document.id,
|
||||
id: envelope.id,
|
||||
status: {
|
||||
not: DocumentStatus.COMPLETED,
|
||||
},
|
||||
@ -209,17 +198,17 @@ const handleDocumentOwnerDelete = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const isDocumentDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
if (!isDocumentDeleteEmailEnabled) {
|
||||
return deletedDocument;
|
||||
if (!isEnvelopeDeleteEmailEnabled) {
|
||||
return deletedEnvelope;
|
||||
}
|
||||
|
||||
// Send cancellation emails to recipients.
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
envelope.recipients.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
@ -227,7 +216,7 @@ const handleDocumentOwnerDelete = async ({
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
@ -258,5 +247,5 @@ const handleDocumentOwnerDelete = async ({
|
||||
}),
|
||||
);
|
||||
|
||||
return deletedDocument;
|
||||
return deletedEnvelope;
|
||||
};
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
import type { Prisma, Recipient } from '@prisma/client';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { nanoid, prefixedId } from '../../universal/id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export interface DuplicateDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export const duplicateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
select: {
|
||||
title: true,
|
||||
userId: true,
|
||||
documentData: {
|
||||
select: {
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
authOptions: true,
|
||||
visibility: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
signingOrder: true,
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const documentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: document.documentData.type,
|
||||
data: document.documentData.initialData,
|
||||
initialData: document.documentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined;
|
||||
|
||||
if (document.documentMeta) {
|
||||
documentMeta = {
|
||||
create: {
|
||||
...omit(document.documentMeta, ['id', 'documentId']),
|
||||
emailSettings: document.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const createdDocument = await prisma.document.create({
|
||||
data: {
|
||||
userId: document.userId,
|
||||
teamId: teamId,
|
||||
title: document.title,
|
||||
documentDataId: documentData.id,
|
||||
authOptions: document.authOptions || undefined,
|
||||
visibility: document.visibility,
|
||||
qrToken: prefixedId('qr'),
|
||||
documentMeta,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientsToCreate = document.recipients.map((recipient) => ({
|
||||
documentId: createdDocument.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
documentId: createdDocument.id,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const recipients: Recipient[] = [];
|
||||
|
||||
for (const recipientData of recipientsToCreate) {
|
||||
const newRecipient = await prisma.recipient.create({
|
||||
data: recipientData,
|
||||
});
|
||||
|
||||
recipients.push(newRecipient);
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse({
|
||||
...mapDocumentToWebhookDocumentPayload(createdDocument),
|
||||
recipients,
|
||||
documentMeta: createdDocument.documentMeta,
|
||||
}),
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
return {
|
||||
documentId: createdDocument.id,
|
||||
};
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentAuditLog, Prisma } from '@prisma/client';
|
||||
import { type DocumentAuditLog, EnvelopeType, type Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -6,7 +6,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface FindDocumentAuditLogsOptions {
|
||||
userId: number;
|
||||
@ -35,22 +35,26 @@ export const findDocumentAuditLogs = async ({
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentAuditLogWhereInput = {
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
};
|
||||
|
||||
// Filter events down to what we consider recent activity.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Document, DocumentSource, Prisma, Team, TeamEmail, User } from '@prisma/client';
|
||||
import { RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import type { DocumentSource, Envelope, Prisma, Team, TeamEmail, User } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -22,7 +22,7 @@ export type FindDocumentsOptions = {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Omit<Document, 'document'>;
|
||||
column: keyof Pick<Envelope, 'createdAt'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
period?: PeriodSelectorValue;
|
||||
@ -69,7 +69,7 @@ export const findDocuments = async ({
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const teamMemberRole = team?.currentTeamRole ?? null;
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
||||
@ -111,7 +111,7 @@ export const findDocuments = async ({
|
||||
},
|
||||
];
|
||||
|
||||
let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user, folderId);
|
||||
let filters: Prisma.EnvelopeWhereInput | null = findDocumentsFilter(status, user, folderId);
|
||||
|
||||
if (team) {
|
||||
filters = findTeamDocumentsFilter(status, team, visibilityFilters, folderId);
|
||||
@ -127,7 +127,7 @@ export const findDocuments = async ({
|
||||
};
|
||||
}
|
||||
|
||||
let deletedFilter: Prisma.DocumentWhereInput = {
|
||||
let deletedFilter: Prisma.EnvelopeWhereInput = {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
@ -180,7 +180,7 @@ export const findDocuments = async ({
|
||||
};
|
||||
}
|
||||
|
||||
const whereAndClause: Prisma.DocumentWhereInput['AND'] = [
|
||||
const whereAndClause: Prisma.EnvelopeWhereInput['AND'] = [
|
||||
{ ...filters },
|
||||
{ ...deletedFilter },
|
||||
{ ...searchFilter },
|
||||
@ -198,7 +198,8 @@ export const findDocuments = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
const whereClause: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
AND: whereAndClause,
|
||||
};
|
||||
|
||||
@ -225,7 +226,7 @@ export const findDocuments = async ({
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.document.findMany({
|
||||
prisma.envelope.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
@ -249,7 +250,7 @@ export const findDocuments = async ({
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
prisma.envelope.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
@ -275,7 +276,7 @@ const findDocumentsFilter = (
|
||||
user: Pick<User, 'id' | 'email' | 'name'>,
|
||||
folderId?: string | null,
|
||||
) => {
|
||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput>(status)
|
||||
return match<ExtendedDocumentStatus, Prisma.EnvelopeWhereInput>(status)
|
||||
.with(ExtendedDocumentStatus.ALL, () => ({
|
||||
OR: [
|
||||
{
|
||||
@ -414,14 +415,14 @@ const findDocumentsFilter = (
|
||||
const findTeamDocumentsFilter = (
|
||||
status: ExtendedDocumentStatus,
|
||||
team: Team & { teamEmail: TeamEmail | null },
|
||||
visibilityFilters: Prisma.DocumentWhereInput[],
|
||||
visibilityFilters: Prisma.EnvelopeWhereInput[],
|
||||
folderId?: string,
|
||||
) => {
|
||||
const teamEmail = team.teamEmail?.email ?? null;
|
||||
|
||||
return match<ExtendedDocumentStatus, Prisma.DocumentWhereInput | null>(status)
|
||||
return match<ExtendedDocumentStatus, Prisma.EnvelopeWhereInput | null>(status)
|
||||
.with(ExtendedDocumentStatus.ALL, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
// Filter to display all documents that belong to the team.
|
||||
OR: [
|
||||
{
|
||||
@ -483,7 +484,7 @@ const findTeamDocumentsFilter = (
|
||||
};
|
||||
})
|
||||
.with(ExtendedDocumentStatus.DRAFT, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
@ -508,7 +509,7 @@ const findTeamDocumentsFilter = (
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.PENDING, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
@ -550,7 +551,7 @@ const findTeamDocumentsFilter = (
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.COMPLETED, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
status: ExtendedDocumentStatus.COMPLETED,
|
||||
OR: [
|
||||
{
|
||||
@ -582,7 +583,7 @@ const findTeamDocumentsFilter = (
|
||||
return filter;
|
||||
})
|
||||
.with(ExtendedDocumentStatus.REJECTED, () => {
|
||||
const filter: Prisma.DocumentWhereInput = {
|
||||
const filter: Prisma.EnvelopeWhereInput = {
|
||||
status: ExtendedDocumentStatus.REJECTED,
|
||||
OR: [
|
||||
{
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
|
||||
export type GetDocumentByAccessTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
@ -9,30 +13,62 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
const result = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
qrToken: token,
|
||||
},
|
||||
// Do not provide extra information that is not needed.
|
||||
select: {
|
||||
id: true,
|
||||
secondaryId: true,
|
||||
internalVersion: true,
|
||||
title: true,
|
||||
completedAt: true,
|
||||
documentData: {
|
||||
team: {
|
||||
select: {
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
title: true,
|
||||
order: true,
|
||||
documentDataId: true,
|
||||
envelopeId: true,
|
||||
documentData: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documentMeta: {
|
||||
_count: {
|
||||
select: {
|
||||
password: true,
|
||||
recipients: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
const firstDocumentData = result.envelopeItems[0].documentData;
|
||||
|
||||
if (!firstDocumentData) {
|
||||
throw new Error('Missing document data');
|
||||
}
|
||||
|
||||
return {
|
||||
id: mapSecondaryIdToDocumentId(result.secondaryId),
|
||||
internalVersion: result.internalVersion,
|
||||
title: result.title,
|
||||
completedAt: result.completedAt,
|
||||
envelopeItems: result.envelopeItems,
|
||||
recipientCount: result._count.recipients,
|
||||
documentTeamUrl: result.team.url,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,156 +0,0 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetDocumentByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const getDocumentById = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
folderId,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
export type GetDocumentWhereInputOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a given Prisma document query.
|
||||
*
|
||||
* This will return a query that allows a user to get a document if they have valid access to it.
|
||||
*/
|
||||
export const getDocumentWhereInput = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWhereInputOptions) => {
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const teamVisibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
])
|
||||
.otherwise(() => [DocumentVisibility.EVERYONE]);
|
||||
|
||||
const documentOrInput: Prisma.DocumentWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
{
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent to or from the team email.
|
||||
if (team.teamEmail) {
|
||||
documentOrInput.push(
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
|
||||
id: documentId,
|
||||
OR: documentOrInput,
|
||||
};
|
||||
|
||||
return {
|
||||
documentWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
|
||||
export interface GetDocumentAndSenderByTokenOptions {
|
||||
@ -39,8 +41,9 @@ export const getDocumentByToken = async ({ token }: GetDocumentByTokenOptions) =
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
const result = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -64,8 +67,9 @@ export const getDocumentAndSenderByToken = async ({
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
const result = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
@ -80,13 +84,17 @@ export const getDocumentAndSenderByToken = async ({
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
@ -102,6 +110,12 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const firstDocumentData = result.envelopeItems[0].documentData;
|
||||
|
||||
if (!firstDocumentData) {
|
||||
throw new Error('Missing document data');
|
||||
}
|
||||
|
||||
const recipient = result.recipients[0];
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
@ -127,6 +141,8 @@ export const getDocumentAndSenderByToken = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(result.secondaryId);
|
||||
|
||||
return {
|
||||
...result,
|
||||
user: {
|
||||
@ -134,64 +150,8 @@ export const getDocumentAndSenderByToken = async ({
|
||||
email: result.user.email,
|
||||
name: result.user.name,
|
||||
},
|
||||
documentData: firstDocumentData,
|
||||
id: legacyDocumentId,
|
||||
envelopeId: result.id,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a Document and a Recipient by the recipient token.
|
||||
*/
|
||||
export const getDocumentAndRecipientByToken = async ({
|
||||
token,
|
||||
userId,
|
||||
accessAuth,
|
||||
requireAccessAuth = true,
|
||||
}: GetDocumentAndRecipientByTokenOptions): Promise<DocumentWithRecipient> => {
|
||||
if (!token) {
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const result = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [recipient] = result.recipients;
|
||||
|
||||
// Sanity check, should not be possible.
|
||||
if (!recipient) {
|
||||
throw new Error('Missing recipient');
|
||||
}
|
||||
|
||||
let documentAccessValid = true;
|
||||
|
||||
if (requireAccessAuth) {
|
||||
documentAccessValid = await isRecipientAuthorized({
|
||||
type: 'ACCESS',
|
||||
documentAuthOptions: result.authOptions,
|
||||
recipient,
|
||||
userId,
|
||||
authOptions: accessAuth,
|
||||
});
|
||||
}
|
||||
|
||||
if (!documentAccessValid) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Invalid access values',
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -4,15 +4,15 @@ import { DOCUMENT_AUDIT_LOG_TYPE, DOCUMENT_EMAIL_TYPE } from '../../types/docume
|
||||
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type GetDocumentCertificateAuditLogsOptions = {
|
||||
id: number;
|
||||
envelopeId: string;
|
||||
};
|
||||
|
||||
export const getDocumentCertificateAuditLogs = async ({
|
||||
id,
|
||||
envelopeId,
|
||||
}: GetDocumentCertificateAuditLogsOptions) => {
|
||||
const rawAuditLogs = await prisma.documentAuditLog.findMany({
|
||||
where: {
|
||||
documentId: id,
|
||||
envelopeId,
|
||||
type: {
|
||||
in: [
|
||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export interface GetDocumentMetaByDocumentIdOptions {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const getDocumentMetaByDocumentId = async ({ id }: GetDocumentMetaByDocumentIdOptions) => {
|
||||
return await prisma.documentMeta.findFirstOrThrow({
|
||||
where: {
|
||||
documentId: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,67 +1,66 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { getEnvelopeById } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetDocumentWithDetailsByIdOptions = {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const getDocumentWithDetailsById = async ({
|
||||
documentId,
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWithDetailsByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const envelope = await getEnvelopeById({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
folder: true,
|
||||
fields: {
|
||||
include: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
const firstDocumentData = envelope.envelopeItems[0].documentData;
|
||||
|
||||
if (!firstDocumentData) {
|
||||
throw new Error('Document data not found');
|
||||
}
|
||||
|
||||
return document;
|
||||
return {
|
||||
...envelope,
|
||||
envelopeId: envelope.id,
|
||||
documentData: {
|
||||
...firstDocumentData,
|
||||
envelopeItemId: envelope.envelopeItems[0].id,
|
||||
},
|
||||
id: legacyDocumentId,
|
||||
fields: envelope.fields.map((field) => ({
|
||||
...field,
|
||||
documentId: legacyDocumentId,
|
||||
templateId: null,
|
||||
})),
|
||||
user: {
|
||||
id: envelope.userId,
|
||||
name: envelope.user.name,
|
||||
email: envelope.user.email,
|
||||
},
|
||||
team: {
|
||||
id: envelope.teamId,
|
||||
url: envelope.team.url,
|
||||
},
|
||||
recipients: envelope.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
documentId: legacyDocumentId,
|
||||
templateId: null,
|
||||
})),
|
||||
documentDataId: firstDocumentData.id,
|
||||
documentMeta: {
|
||||
...envelope.documentMeta,
|
||||
documentId: legacyDocumentId,
|
||||
password: null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ export type GetRecipientOrSenderByShareLinkSlugOptions = {
|
||||
export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||
slug,
|
||||
}: GetRecipientOrSenderByShareLinkSlugOptions) => {
|
||||
const { documentId, email } = await prisma.documentShareLink.findFirstOrThrow({
|
||||
const { envelopeId, email } = await prisma.documentShareLink.findFirstOrThrow({
|
||||
where: {
|
||||
slug,
|
||||
},
|
||||
@ -15,7 +15,7 @@ export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||
|
||||
const sender = await prisma.user.findFirst({
|
||||
where: {
|
||||
documents: { some: { id: documentId } },
|
||||
envelopes: { some: { id: envelopeId } },
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
@ -31,7 +31,7 @@ export const getRecipientOrSenderByShareLinkSlug = async ({
|
||||
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
envelopeId,
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { EnvelopeType, TeamMemberRole } from '@prisma/client';
|
||||
import type { Prisma, User } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
@ -25,7 +25,7 @@ export const getStats = async ({
|
||||
folderId,
|
||||
...options
|
||||
}: GetStatsInput) => {
|
||||
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
let createdAt: Prisma.EnvelopeWhereInput['createdAt'];
|
||||
|
||||
if (period) {
|
||||
const daysAgo = parseInt(period.replace(/d$/, ''), 10);
|
||||
@ -90,13 +90,13 @@ export const getStats = async ({
|
||||
|
||||
type GetCountsOption = {
|
||||
user: Pick<User, 'id' | 'email'>;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
createdAt: Prisma.EnvelopeWhereInput['createdAt'];
|
||||
search?: string;
|
||||
folderId?: string | null;
|
||||
};
|
||||
|
||||
const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) => {
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||
@ -108,12 +108,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
|
||||
return Promise.all([
|
||||
// Owner counts.
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: user.id,
|
||||
createdAt,
|
||||
deletedAt: null,
|
||||
@ -121,12 +122,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
},
|
||||
}),
|
||||
// Not signed counts.
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: ExtendedDocumentStatus.PENDING,
|
||||
recipients: {
|
||||
some: {
|
||||
@ -140,12 +142,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption)
|
||||
},
|
||||
}),
|
||||
// Has signed counts.
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
createdAt,
|
||||
user: {
|
||||
email: {
|
||||
@ -186,7 +189,7 @@ type GetTeamCountsOption = {
|
||||
senderIds?: number[];
|
||||
currentUserEmail: string;
|
||||
userId: number;
|
||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||
createdAt: Prisma.EnvelopeWhereInput['createdAt'];
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
search?: string;
|
||||
folderId?: string | null;
|
||||
@ -197,14 +200,14 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
|
||||
const senderIds = options.senderIds ?? [];
|
||||
|
||||
const userIdWhereClause: Prisma.DocumentWhereInput['userId'] =
|
||||
const userIdWhereClause: Prisma.EnvelopeWhereInput['userId'] =
|
||||
senderIds.length > 0
|
||||
? {
|
||||
in: senderIds,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const searchFilter: Prisma.DocumentWhereInput = {
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = {
|
||||
OR: [
|
||||
{ title: { contains: options.search, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||
@ -212,7 +215,8 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
],
|
||||
};
|
||||
|
||||
let ownerCountsWhereInput: Prisma.DocumentWhereInput = {
|
||||
let ownerCountsWhereInput: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
teamId,
|
||||
@ -223,7 +227,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
let notSignedCountsGroupByArgs = null;
|
||||
let hasSignedCountsGroupByArgs = null;
|
||||
|
||||
const visibilityFiltersWhereInput: Prisma.DocumentWhereInput = {
|
||||
const visibilityFiltersWhereInput: Prisma.EnvelopeWhereInput = {
|
||||
AND: [
|
||||
{ deletedAt: null },
|
||||
{
|
||||
@ -267,6 +271,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
|
||||
if (teamEmail) {
|
||||
ownerCountsWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
OR: [
|
||||
@ -288,6 +293,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
folderId,
|
||||
@ -301,7 +307,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
} satisfies Prisma.DocumentGroupByArgs;
|
||||
} satisfies Prisma.EnvelopeGroupByArgs;
|
||||
|
||||
hasSignedCountsGroupByArgs = {
|
||||
by: ['status'],
|
||||
@ -309,6 +315,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
_all: true,
|
||||
},
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: userIdWhereClause,
|
||||
createdAt,
|
||||
folderId,
|
||||
@ -336,18 +343,18 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies Prisma.DocumentGroupByArgs;
|
||||
} satisfies Prisma.EnvelopeGroupByArgs;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
prisma.document.groupBy({
|
||||
prisma.envelope.groupBy({
|
||||
by: ['status'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
where: ownerCountsWhereInput,
|
||||
}),
|
||||
notSignedCountsGroupByArgs ? prisma.document.groupBy(notSignedCountsGroupByArgs) : [],
|
||||
hasSignedCountsGroupByArgs ? prisma.document.groupBy(hasSignedCountsGroupByArgs) : [],
|
||||
notSignedCountsGroupByArgs ? prisma.envelope.groupBy(notSignedCountsGroupByArgs) : [],
|
||||
hasSignedCountsGroupByArgs ? prisma.envelope.groupBy(hasSignedCountsGroupByArgs) : [],
|
||||
]);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Document, Recipient } from '@prisma/client';
|
||||
import type { Envelope, Recipient } from '@prisma/client';
|
||||
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -17,8 +17,8 @@ import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
type IsRecipientAuthorizedOptions = {
|
||||
// !: Probably find a better name than 'ACCESS_2FA' if requirements change.
|
||||
type: 'ACCESS' | 'ACCESS_2FA' | 'ACTION';
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email' | 'documentId'>;
|
||||
documentAuthOptions: Envelope['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email' | 'envelopeId'>;
|
||||
|
||||
/**
|
||||
* The ID of the user who initiated the request.
|
||||
@ -125,14 +125,8 @@ export const isRecipientAuthorized = async ({
|
||||
}
|
||||
|
||||
if (type === 'ACCESS_2FA' && method === 'email') {
|
||||
if (!recipient.documentId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document ID is required for email 2FA verification',
|
||||
});
|
||||
}
|
||||
|
||||
return await validateTwoFactorTokenFromEmail({
|
||||
documentId: recipient.documentId,
|
||||
envelopeId: recipient.envelopeId,
|
||||
email: recipient.email,
|
||||
code: token,
|
||||
window: 10, // 5 minutes worth of tokens
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -7,17 +7,19 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
|
||||
export type RejectDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
reason: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export async function rejectDocumentWithToken({
|
||||
token,
|
||||
documentId,
|
||||
id,
|
||||
reason,
|
||||
requestMetadata,
|
||||
}: RejectDocumentWithTokenOptions) {
|
||||
@ -25,16 +27,16 @@ export async function rejectDocumentWithToken({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
documentId,
|
||||
envelope: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
envelope: true,
|
||||
},
|
||||
});
|
||||
|
||||
const document = recipient?.document;
|
||||
const envelope = recipient?.envelope;
|
||||
|
||||
if (!recipient || !document) {
|
||||
if (!recipient || !envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document or recipient not found',
|
||||
});
|
||||
@ -54,7 +56,7 @@ export async function rejectDocumentWithToken({
|
||||
}),
|
||||
prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
@ -72,11 +74,13 @@ export async function rejectDocumentWithToken({
|
||||
}),
|
||||
]);
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
// Trigger the seal document job to process the document asynchronously
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
requestMetadata,
|
||||
},
|
||||
});
|
||||
@ -86,7 +90,7 @@ export async function rejectDocumentWithToken({
|
||||
name: 'send.signing.rejected.emails',
|
||||
payload: {
|
||||
recipientId: recipient.id,
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -94,7 +98,7 @@ export async function rejectDocumentWithToken({
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.cancelled.emails',
|
||||
payload: {
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
cancellationReason: reason,
|
||||
requestMetadata,
|
||||
},
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
@ -19,13 +25,14 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { calculateRecipientExpiry } from '../../utils/expiry';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
recipients: number[];
|
||||
teamId: number;
|
||||
@ -33,7 +40,7 @@ export type ResendDocumentOptions = {
|
||||
};
|
||||
|
||||
export const resendDocument = async ({
|
||||
documentId,
|
||||
id,
|
||||
userId,
|
||||
recipients,
|
||||
teamId,
|
||||
@ -43,16 +50,22 @@ export const resendDocument = async ({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
@ -65,31 +78,29 @@ export const resendDocument = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const customEmail = document?.documentMeta;
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.DRAFT) {
|
||||
if (envelope.status === DocumentStatus.DRAFT) {
|
||||
throw new Error('Can not send draft document');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const recipientsToRemind = document.recipients.filter(
|
||||
const recipientsToRemind = envelope.recipients.filter(
|
||||
(recipient) =>
|
||||
recipients.includes(recipient.id) && recipient.signingStatus === SigningStatus.NOT_SIGNED,
|
||||
);
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
@ -101,9 +112,9 @@ export const resendDocument = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
@ -123,42 +134,42 @@ export const resendDocument = async ({
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
.toLowerCase();
|
||||
|
||||
let emailMessage = customEmail?.message || '';
|
||||
let emailMessage = envelope.documentMeta.message || '';
|
||||
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
emailMessage =
|
||||
customEmail?.message ||
|
||||
envelope.documentMeta.message ||
|
||||
i18n._(
|
||||
msg`${user.name || user.email} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
msg`${user.name || user.email} on behalf of "${envelope.team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': document.title,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? document.team?.teamEmail?.email || user.email
|
||||
? envelope.team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
@ -166,7 +177,7 @@ export const resendDocument = async ({
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
organisationType,
|
||||
teamName: document.team?.name,
|
||||
teamName: envelope.team?.name,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
@ -190,9 +201,9 @@ export const resendDocument = async ({
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: customEmail?.subject
|
||||
subject: envelope.documentMeta.subject
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${customEmail.subject}`),
|
||||
i18n._(msg`Reminder: ${envelope.documentMeta.subject}`),
|
||||
customEmailTemplate,
|
||||
)
|
||||
: emailSubject,
|
||||
@ -200,11 +211,11 @@ export const resendDocument = async ({
|
||||
text,
|
||||
});
|
||||
|
||||
if (document.documentMeta?.expiryAmount && document.documentMeta?.expiryUnit) {
|
||||
if (envelope.documentMeta?.expiryAmount && envelope.documentMeta?.expiryUnit) {
|
||||
const previousExpiryDate = recipient.expired;
|
||||
const newExpiryDate = calculateRecipientExpiry(
|
||||
document.documentMeta.expiryAmount,
|
||||
document.documentMeta.expiryUnit,
|
||||
envelope.documentMeta.expiryAmount,
|
||||
envelope.documentMeta.expiryUnit,
|
||||
new Date(),
|
||||
);
|
||||
|
||||
@ -220,7 +231,7 @@ export const resendDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRY_EXTENDED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientId: recipient.id,
|
||||
@ -236,7 +247,7 @@ export const resendDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
|
||||
@ -1,267 +0,0 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'node:path';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||
import { flattenForm } from '../pdf/flatten-form';
|
||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||
import { legacy_insertFieldInPDF } from '../pdf/legacy-insert-field-in-pdf';
|
||||
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { sendCompletedEmail } from './send-completed-email';
|
||||
|
||||
export type SealDocumentOptions = {
|
||||
documentId: number;
|
||||
sendEmail?: boolean;
|
||||
isResealing?: boolean;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const sealDocument = async ({
|
||||
documentId,
|
||||
sendEmail = true,
|
||||
isResealing = false,
|
||||
requestMetadata,
|
||||
}: SealDocumentOptions) => {
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { documentData } = document;
|
||||
|
||||
if (!documentData) {
|
||||
throw new Error(`Document ${document.id} has no document data`);
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
});
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Determine if the document has been rejected by checking if any recipient has rejected it
|
||||
const rejectedRecipient = recipients.find(
|
||||
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
|
||||
);
|
||||
|
||||
const isRejected = Boolean(rejectedRecipient);
|
||||
|
||||
// Get the rejection reason from the rejected recipient
|
||||
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
|
||||
|
||||
// If the document is not rejected, ensure all recipients have signed
|
||||
if (
|
||||
!isRejected &&
|
||||
recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED)
|
||||
) {
|
||||
throw new Error(`Document ${document.id} has unsigned recipients`);
|
||||
}
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Skip the field check if the document is rejected
|
||||
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
|
||||
throw new Error(`Document ${document.id} has unsigned required fields`);
|
||||
}
|
||||
|
||||
if (isResealing) {
|
||||
// If we're resealing we want to use the initial data for the document
|
||||
// so we aren't placing fields on top of eachother.
|
||||
documentData.data = documentData.initialData;
|
||||
}
|
||||
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFileServerSide(documentData);
|
||||
|
||||
const certificateData = settings.includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get certificate PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const auditLogData = settings.includeAuditLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch((e) => {
|
||||
console.log('Failed to get audit logs PDF');
|
||||
console.error(e);
|
||||
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
normalizeSignatureAppearances(doc);
|
||||
await flattenForm(doc);
|
||||
flattenAnnotations(doc);
|
||||
|
||||
// Add rejection stamp if the document is rejected
|
||||
if (isRejected && rejectionReason) {
|
||||
await addRejectionStampToPdf(doc, rejectionReason);
|
||||
}
|
||||
|
||||
if (certificateData) {
|
||||
const certificate = await PDFDocument.load(certificateData);
|
||||
|
||||
const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
|
||||
|
||||
certificatePages.forEach((page) => {
|
||||
doc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLog = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
doc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
document.useLegacyFieldInsertion
|
||||
? await legacy_insertFieldInPDF(doc, field)
|
||||
: await insertFieldInPDF(doc, field);
|
||||
}
|
||||
|
||||
// Re-flatten post-insertion to handle fields that create arcoFields
|
||||
await flattenForm(doc);
|
||||
|
||||
const pdfBytes = await doc.save();
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name } = path.parse(document.title);
|
||||
|
||||
// Add suffix based on document status
|
||||
const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf';
|
||||
|
||||
const { data: newData } = await putPdfFileServerSide({
|
||||
name: `${name}${suffix}`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
|
||||
const postHog = PostHogServerClient();
|
||||
|
||||
if (postHog) {
|
||||
postHog.capture({
|
||||
distinctId: nanoid(),
|
||||
event: 'App: Document Sealed',
|
||||
properties: {
|
||||
documentId: document.id,
|
||||
isRejected,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.document.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
data: {
|
||||
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentData.update({
|
||||
where: {
|
||||
id: documentData.id,
|
||||
},
|
||||
data: {
|
||||
data: newData,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||
documentId: document.id,
|
||||
requestMetadata,
|
||||
user: null,
|
||||
data: {
|
||||
transactionId: nanoid(),
|
||||
...(isRejected ? { isRejected: true, rejectionReason } : {}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
if (sendEmail && !isResealing) {
|
||||
await sendCompletedEmail({ documentId, requestMetadata });
|
||||
}
|
||||
|
||||
const updatedDocument = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: isRejected
|
||||
? WebhookTriggerEvents.DOCUMENT_REJECTED
|
||||
: WebhookTriggerEvents.DOCUMENT_COMPLETED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
});
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import type { Document, Recipient, User } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import type { Envelope, Recipient, User } from '@prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
} from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
|
||||
export type SearchDocumentsWithKeywordOptions = {
|
||||
query: string;
|
||||
userId: number;
|
||||
@ -19,7 +21,7 @@ export type SearchDocumentsWithKeywordOptions = {
|
||||
export const searchDocumentsWithKeyword = async ({
|
||||
query,
|
||||
userId,
|
||||
limit = 5,
|
||||
limit = 20,
|
||||
}: SearchDocumentsWithKeywordOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
@ -27,8 +29,9 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const documents = await prisma.document.findMany({
|
||||
const envelopes = await prisma.envelope.findMany({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
@ -122,31 +125,33 @@ export const searchDocumentsWithKeyword = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
distinct: ['id'],
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: limit,
|
||||
});
|
||||
|
||||
const isOwner = (document: Document, user: User) => document.userId === user.id;
|
||||
const isOwner = (envelope: Envelope, user: User) => envelope.userId === user.id;
|
||||
|
||||
const getSigningLink = (recipients: Recipient[], user: User) =>
|
||||
`/sign/${recipients.find((r) => r.email === user.email)?.token}`;
|
||||
|
||||
const maskedDocuments = documents
|
||||
.filter((document) => {
|
||||
if (!document.teamId || isOwner(document, user)) {
|
||||
const maskedDocuments = envelopes
|
||||
.filter((envelope) => {
|
||||
if (!envelope.teamId || isOwner(envelope, user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const teamMemberRole = getHighestTeamRoleInGroup(
|
||||
document.team.teamGroups.filter((tg) => tg.teamId === document.teamId),
|
||||
envelope.team.teamGroups.filter((tg) => tg.teamId === envelope.teamId),
|
||||
);
|
||||
|
||||
if (!teamMemberRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canAccessDocument = match([document.visibility, teamMemberRole])
|
||||
const canAccessDocument = match([envelope.visibility, teamMemberRole])
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
|
||||
.with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
|
||||
@ -157,23 +162,29 @@ export const searchDocumentsWithKeyword = async ({
|
||||
|
||||
return canAccessDocument;
|
||||
})
|
||||
.map((document) => {
|
||||
const { recipients, ...documentWithoutRecipient } = document;
|
||||
.map((envelope) => {
|
||||
const { recipients, ...documentWithoutRecipient } = envelope;
|
||||
|
||||
let documentPath;
|
||||
|
||||
if (isOwner(document, user)) {
|
||||
documentPath = `${formatDocumentsPath(document.team?.url)}/${document.id}`;
|
||||
} else if (document.teamId && document.team) {
|
||||
documentPath = `${formatDocumentsPath(document.team.url)}/${document.id}`;
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
if (isOwner(envelope, user)) {
|
||||
documentPath = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`;
|
||||
} else if (envelope.teamId && envelope.team.teamGroups.length > 0) {
|
||||
documentPath = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`;
|
||||
} else {
|
||||
documentPath = getSigningLink(recipients, user);
|
||||
}
|
||||
|
||||
return {
|
||||
...documentWithoutRecipient,
|
||||
team: {
|
||||
id: envelope.teamId,
|
||||
url: envelope.team.url,
|
||||
},
|
||||
path: documentPath,
|
||||
value: [document.id, document.title, ...document.recipients.map((r) => r.email)].join(' '),
|
||||
value: [envelope.id, envelope.title, ...envelope.recipients.map((r) => r.email)].join(' '),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentSource } from '@prisma/client';
|
||||
import { DocumentSource, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||
@ -14,23 +14,33 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendDocumentOptions {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDocumentOptions) => {
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOptions) => {
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
select: {
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
user: {
|
||||
@ -49,13 +59,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
const isDirectTemplate = envelope?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -63,28 +73,38 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { user: owner } = document;
|
||||
const { user: owner } = envelope;
|
||||
|
||||
const completedDocument = await getFileServerSide(document.documentData);
|
||||
const completedDocumentEmailAttachments = await Promise.all(
|
||||
envelope.envelopeItems.map(async (document) => {
|
||||
const file = await getFileServerSide(document.documentData);
|
||||
|
||||
return {
|
||||
fileName: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
content: Buffer.from(file),
|
||||
contentType: 'application/pdf',
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(
|
||||
document.team?.url,
|
||||
)}/${document.id}`;
|
||||
envelope.team?.url,
|
||||
)}/${envelope.id}`;
|
||||
|
||||
if (document.team?.url) {
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${
|
||||
document.id
|
||||
if (envelope.team?.url) {
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${
|
||||
envelope.id
|
||||
}`;
|
||||
}
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
@ -95,11 +115,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
// - Recipient emails are disabled
|
||||
if (
|
||||
isOwnerDocumentCompletedEmailEnabled &&
|
||||
(!document.recipients.find((recipient) => recipient.email === owner.email) ||
|
||||
(!envelope.recipients.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled)
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: documentOwnerDownloadLink,
|
||||
});
|
||||
@ -127,18 +147,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
subject: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
content: Buffer.from(completedDocument),
|
||||
},
|
||||
],
|
||||
attachments: completedDocumentEmailAttachments,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
@ -158,22 +173,22 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
document.recipients.map(async (recipient) => {
|
||||
envelope.recipients.map(async (recipient) => {
|
||||
const customEmailTemplate = {
|
||||
'signer.name': recipient.name,
|
||||
'signer.email': recipient.email,
|
||||
'document.name': document.title,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
|
||||
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
|
||||
customBody:
|
||||
isDirectTemplate && document.documentMeta?.message
|
||||
? renderCustomEmailTemplate(document.documentMeta.message, customEmailTemplate)
|
||||
isDirectTemplate && envelope.documentMeta?.message
|
||||
? renderCustomEmailTemplate(envelope.documentMeta.message, customEmailTemplate)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@ -198,23 +213,18 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject:
|
||||
isDirectTemplate && document.documentMeta?.subject
|
||||
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
|
||||
isDirectTemplate && envelope.documentMeta?.subject
|
||||
? renderCustomEmailTemplate(envelope.documentMeta.subject, customEmailTemplate)
|
||||
: i18n._(msg`Signing Complete!`),
|
||||
html,
|
||||
text,
|
||||
attachments: [
|
||||
{
|
||||
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
|
||||
content: Buffer.from(completedDocument),
|
||||
},
|
||||
],
|
||||
attachments: completedDocumentEmailAttachments,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: null,
|
||||
requestMetadata,
|
||||
data: {
|
||||
|
||||
@ -14,14 +14,15 @@ import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendDeleteEmailOptions {
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
// Note: Currently only sent by Admin function
|
||||
export const sendDeleteEmail = async ({ envelopeId, reason }: SendDeleteEmailOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
@ -35,14 +36,14 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
if (!isDocumentDeletedEmailEnabled) {
|
||||
@ -53,17 +54,17 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { email, name } = document.user;
|
||||
const { email, name } = envelope.user;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentSuperDeleteEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
reason,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { DocumentData, Envelope, EnvelopeItem } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@ -16,18 +18,19 @@ import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { calculateRecipientExpiry } from '../../utils/expiry';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
sendEmail?: boolean;
|
||||
@ -35,93 +38,81 @@ export type SendDocumentOptions = {
|
||||
};
|
||||
|
||||
export const sendDocument = async ({
|
||||
documentId,
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
sendEmail,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||
},
|
||||
documentMeta: true,
|
||||
documentData: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
documentData: {
|
||||
select: {
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(document.status)) {
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
let recipientsToNotify = document.recipients;
|
||||
const signingOrder = envelope.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
|
||||
|
||||
let recipientsToNotify = envelope.recipients;
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
// Get the currently active recipient.
|
||||
recipientsToNotify = document.recipients
|
||||
recipientsToNotify = envelope.recipients
|
||||
.filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC)
|
||||
.slice(0, 1);
|
||||
|
||||
// Secondary filter so we aren't resending if the current active recipient has already
|
||||
// received the document.
|
||||
// received the envelope.
|
||||
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
|
||||
}
|
||||
|
||||
const { documentData } = document;
|
||||
|
||||
if (!documentData.data) {
|
||||
throw new Error('Document data not found');
|
||||
if (envelope.envelopeItems.length === 0) {
|
||||
throw new Error('Missing envelope items');
|
||||
}
|
||||
|
||||
if (document.formValues) {
|
||||
const file = await getFileServerSide(documentData);
|
||||
|
||||
const prefilled = await insertFormValuesInPdf({
|
||||
pdf: Buffer.from(file),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
formValues: document.formValues as Record<string, string | number | boolean>,
|
||||
});
|
||||
|
||||
let fileName = document.title;
|
||||
|
||||
if (!document.title.endsWith('.pdf')) {
|
||||
fileName = `${document.title}.pdf`;
|
||||
}
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
});
|
||||
|
||||
const result = await prisma.document.update({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
data: {
|
||||
documentDataId: newDocumentData.id,
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(document, result);
|
||||
if (envelope.formValues) {
|
||||
await Promise.all(
|
||||
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||
await injectFormValuesIntoDocument(envelope, envelopeItem);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
||||
@ -134,7 +125,7 @@ export const sendDocument = async ({
|
||||
// const fieldsWithSignerEmail = fields.map((field) => ({
|
||||
// ...field,
|
||||
// signerEmail:
|
||||
// document.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
// envelope.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
// }));
|
||||
|
||||
// const everySignerHasSignature = document?.Recipient.every(
|
||||
@ -149,7 +140,7 @@ export const sendDocument = async ({
|
||||
// throw new Error('Some signers have not been assigned a signature field.');
|
||||
// }
|
||||
|
||||
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
||||
const allRecipientsHaveNoActionToTake = envelope.recipients.every(
|
||||
(recipient) =>
|
||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
||||
);
|
||||
@ -158,15 +149,15 @@ export const sendDocument = async ({
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.seal-document',
|
||||
payload: {
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the return type the same for the `sendDocument` method
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
return await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
@ -175,28 +166,28 @@ export const sendDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDocument = await prisma.$transaction(async (tx) => {
|
||||
if (document.status === DocumentStatus.DRAFT) {
|
||||
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
if (envelope.status === DocumentStatus.DRAFT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentMeta?.expiryAmount && document.documentMeta?.expiryUnit) {
|
||||
if (envelope.documentMeta?.expiryAmount && envelope.documentMeta?.expiryUnit) {
|
||||
const expiryDate = calculateRecipientExpiry(
|
||||
document.documentMeta.expiryAmount,
|
||||
document.documentMeta.expiryUnit,
|
||||
envelope.documentMeta.expiryAmount,
|
||||
envelope.documentMeta.expiryUnit,
|
||||
new Date(), // Calculate from current time
|
||||
);
|
||||
|
||||
await tx.recipient.updateMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
expired: null,
|
||||
},
|
||||
data: {
|
||||
@ -205,9 +196,9 @@ export const sendDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.document.update({
|
||||
return await tx.envelope.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
status: DocumentStatus.PENDING,
|
||||
@ -220,7 +211,7 @@ export const sendDocument = async ({
|
||||
});
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
// Only send email if one of the following is true:
|
||||
@ -237,7 +228,7 @@ export const sendDocument = async ({
|
||||
name: 'send.signing.requested.email',
|
||||
payload: {
|
||||
userId,
|
||||
documentId,
|
||||
documentId: legacyDocumentId,
|
||||
recipientId: recipient.id,
|
||||
requestMetadata: requestMetadata?.requestMetadata,
|
||||
},
|
||||
@ -248,10 +239,44 @@ export const sendDocument = async ({
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_SENT,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
return updatedEnvelope;
|
||||
};
|
||||
|
||||
const injectFormValuesIntoDocument = async (
|
||||
envelope: Envelope,
|
||||
envelopeItem: Pick<EnvelopeItem, 'id'> & { documentData: DocumentData },
|
||||
) => {
|
||||
const file = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
const prefilled = await insertFormValuesInPdf({
|
||||
pdf: Buffer.from(file),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
formValues: envelope.formValues as Record<string, string | number | boolean>,
|
||||
});
|
||||
|
||||
let fileName = envelope.title;
|
||||
|
||||
if (!envelope.title.endsWith('.pdf')) {
|
||||
fileName = `${envelope.title}.pdf`;
|
||||
}
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||
});
|
||||
|
||||
await prisma.envelopeItem.update({
|
||||
where: {
|
||||
id: envelopeItem.id,
|
||||
},
|
||||
data: {
|
||||
documentDataId: newDocumentData.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
||||
@ -9,18 +10,20 @@ import { prisma } from '@documenso/prisma';
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export interface SendPendingEmailOptions {
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipientId: number;
|
||||
}
|
||||
|
||||
export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
@ -37,11 +40,11 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.recipients.length === 0) {
|
||||
if (envelope.recipients.length === 0) {
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
@ -49,27 +52,27 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).documentPending;
|
||||
|
||||
if (!isDocumentPendingEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [recipient] = document.recipients;
|
||||
const [recipient] = envelope.recipients;
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentPendingEmailTemplate, {
|
||||
documentName: document.title,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
|
||||
@ -1,248 +0,0 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
data?: {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
};
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const updateDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isDocumentOwner = document.userId === userId;
|
||||
const requestedVisibility = data?.visibility;
|
||||
|
||||
if (!isDocumentOwner) {
|
||||
match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => {
|
||||
const allowedVisibilities: DocumentVisibility[] = [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
];
|
||||
|
||||
if (
|
||||
!allowedVisibilities.includes(document.visibility) ||
|
||||
(requestedVisibility && !allowedVisibilities.includes(requestedVisibility))
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.with(TeamMemberRole.MEMBER, () => {
|
||||
if (
|
||||
document.visibility !== DocumentVisibility.EVERYONE ||
|
||||
(requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document visibility',
|
||||
});
|
||||
}
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the document',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If no data just return the document since this function is normally chained after a meta update.
|
||||
if (!data || Object.values(data).length === 0) {
|
||||
return document;
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth.length > 0 && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined ||
|
||||
isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined ||
|
||||
isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && document.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the document has been sent',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTitleSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
to: newGlobalAccessAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalActionSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
to: newGlobalActionAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: document.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0 && data.useLegacyFieldInsertion === undefined) {
|
||||
return document;
|
||||
}
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const updatedDocument = await tx.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
useLegacyFieldInsertion: data.useLegacyFieldInsertion,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: auditLogs,
|
||||
});
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Document, Field, Recipient } from '@prisma/client';
|
||||
import type { Envelope, Field, Recipient } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@ -6,8 +6,8 @@ import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
|
||||
export type ValidateFieldAuthOptions = {
|
||||
documentAuthOptions: Document['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email' | 'documentId'>;
|
||||
documentAuthOptions: Envelope['authOptions'];
|
||||
recipient: Pick<Recipient, 'authOptions' | 'email' | 'envelopeId'>;
|
||||
field: Field;
|
||||
userId?: number;
|
||||
authOptions?: TRecipientActionAuth;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReadStatus, SendStatus } from '@prisma/client';
|
||||
import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -9,7 +9,7 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { TDocumentAccessAuthTypes } from '../../types/document-auth';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
@ -27,19 +27,30 @@ export const viewedDocument = async ({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
envelope: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
envelope: {
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient || !recipient.documentId) {
|
||||
if (!recipient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { documentId } = recipient;
|
||||
const { envelope } = recipient;
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -75,7 +86,7 @@ export const viewedDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
@ -92,24 +103,10 @@ export const viewedDocument = async ({
|
||||
});
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_OPENED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)),
|
||||
userId: document.userId,
|
||||
teamId: document.teamId ?? undefined,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
};
|
||||
|
||||
376
packages/lib/server-only/envelope/create-envelope.ts
Normal file
376
packages/lib/server-only/envelope/create-envelope.ts
Normal file
@ -0,0 +1,376 @@
|
||||
import type { DocumentMeta, DocumentVisibility, TemplateType } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
FolderType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { extractDerivedDocumentMeta } from '../../utils/document';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateEnvelopeOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
normalizePdf?: boolean;
|
||||
internalVersion: 1 | 2;
|
||||
data: {
|
||||
type: EnvelopeType;
|
||||
title: string;
|
||||
externalId?: string;
|
||||
envelopeItems: { title?: string; documentDataId: string; order?: number }[];
|
||||
formValues?: TDocumentFormValues;
|
||||
|
||||
timezone?: string;
|
||||
userTimezone?: string;
|
||||
|
||||
templateType?: TemplateType;
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
recipients?: TCreateEnvelopeRequest['recipients'];
|
||||
folderId?: string;
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const createEnvelope = async ({
|
||||
userId,
|
||||
teamId,
|
||||
normalizePdf,
|
||||
data,
|
||||
meta,
|
||||
requestMetadata,
|
||||
internalVersion,
|
||||
}: CreateEnvelopeOptions) => {
|
||||
const {
|
||||
type,
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
timezone,
|
||||
userTimezone,
|
||||
folderId,
|
||||
templateType,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
publicTitle,
|
||||
publicDescription,
|
||||
visibility: visibilityOverride,
|
||||
} = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify that the folder exists and is associated with the team.
|
||||
if (folderId) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: folderId,
|
||||
type: data.type === EnvelopeType.TEMPLATE ? FolderType.TEMPLATE : FolderType.DOCUMENT,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (data.envelopeItems.length !== 1 && internalVersion === 1) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Envelope items must have exactly 1 item for version 1',
|
||||
});
|
||||
}
|
||||
|
||||
let envelopeItems: { title?: string; documentDataId: string; order?: number }[] =
|
||||
data.envelopeItems;
|
||||
|
||||
if (normalizePdf) {
|
||||
envelopeItems = await Promise.all(
|
||||
data.envelopeItems.map(async (item) => {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: item.documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!documentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = await getFileServerSide(documentData);
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
const titleToUse = item.title || title;
|
||||
|
||||
const newDocumentData = await putPdfFileServerSide({
|
||||
name: titleToUse,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
|
||||
return {
|
||||
title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
|
||||
documentDataId: newDocumentData.id,
|
||||
order: item.order,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: globalAccessAuth || [],
|
||||
globalActionAuth: globalActionAuth || [],
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = data.recipients?.some(
|
||||
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (
|
||||
(authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) &&
|
||||
!team.organisation.organisationClaim.flags.cfr21
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const visibility = visibilityOverride || settings.documentVisibility;
|
||||
|
||||
const emailId = meta?.emailId;
|
||||
|
||||
// Validate that the email ID belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// userTimezone is last because it's always passed in regardless of the organisation/team settings
|
||||
// for uploads from the frontend
|
||||
const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: extractDerivedDocumentMeta(settings, {
|
||||
...meta,
|
||||
timezone: timezoneToUse,
|
||||
}),
|
||||
});
|
||||
|
||||
const secondaryId =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const envelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId,
|
||||
internalVersion,
|
||||
type,
|
||||
title,
|
||||
qrToken: prefixedId('qr'),
|
||||
externalId,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
data: envelopeItems.map((item, i) => ({
|
||||
id: prefixedId('envelope_item'),
|
||||
title: item.title || title,
|
||||
order: item.order !== undefined ? item.order : i + 1,
|
||||
documentDataId: item.documentDataId,
|
||||
})),
|
||||
},
|
||||
},
|
||||
userId,
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
folderId,
|
||||
formValues,
|
||||
source: type === EnvelopeType.DOCUMENT ? DocumentSource.DOCUMENT : DocumentSource.TEMPLATE,
|
||||
documentMetaId: documentMeta.id,
|
||||
|
||||
// Template specific fields.
|
||||
templateType: type === EnvelopeType.TEMPLATE ? templateType : undefined,
|
||||
publicTitle: type === EnvelopeType.TEMPLATE ? publicTitle : undefined,
|
||||
publicDescription: type === EnvelopeType.TEMPLATE ? publicDescription : undefined,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
const firstEnvelopeItem = envelope.envelopeItems[0];
|
||||
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
});
|
||||
|
||||
const recipientFieldsToCreate = (recipient.fields || []).map((field) => {
|
||||
let envelopeItemId = firstEnvelopeItem.id;
|
||||
|
||||
if (field.documentDataId) {
|
||||
const foundEnvelopeItem = envelope.envelopeItems.find(
|
||||
(item) => item.documentDataId === field.documentDataId,
|
||||
);
|
||||
|
||||
if (!foundEnvelopeItem) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
envelopeItemId = foundEnvelopeItem.id;
|
||||
}
|
||||
|
||||
return {
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId,
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
await tx.recipient.create({
|
||||
data: {
|
||||
envelopeId: envelope.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: recipientAuthOptions,
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipientFieldsToCreate,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const createdEnvelope = await tx.envelope.findFirst({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
folder: true,
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdEnvelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Only create audit logs and webhook events for documents.
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
type: DocumentSource.DOCUMENT,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return createdEnvelope;
|
||||
});
|
||||
};
|
||||
196
packages/lib/server-only/envelope/duplicate-envelope.ts
Normal file
196
packages/lib/server-only/envelope/duplicate-envelope.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { DocumentSource, EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
|
||||
import { omit } from 'remeda';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { nanoid, prefixedId } from '../../universal/id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export interface DuplicateEnvelopeOptions {
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelopeOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
select: {
|
||||
type: true,
|
||||
title: true,
|
||||
userId: true,
|
||||
internalVersion: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
select: {
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
authOptions: true,
|
||||
visibility: true,
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
signingOrder: true,
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { legacyNumberId, secondaryId } =
|
||||
envelope.type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then(({ documentId, formattedDocumentId }) => ({
|
||||
legacyNumberId: documentId,
|
||||
secondaryId: formattedDocumentId,
|
||||
}))
|
||||
: await incrementTemplateId().then(({ templateId, formattedTemplateId }) => ({
|
||||
legacyNumberId: templateId,
|
||||
secondaryId: formattedTemplateId,
|
||||
}));
|
||||
|
||||
const createdDocumentMeta = await prisma.documentMeta.create({
|
||||
data: {
|
||||
...omit(envelope.documentMeta, ['id']),
|
||||
emailSettings: envelope.documentMeta.emailSettings || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicatedEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId,
|
||||
type: envelope.type,
|
||||
internalVersion: envelope.internalVersion,
|
||||
userId,
|
||||
teamId,
|
||||
title: envelope.title + ' (copy)',
|
||||
documentMetaId: createdDocumentMeta.id,
|
||||
authOptions: envelope.authOptions || undefined,
|
||||
visibility: envelope.visibility,
|
||||
source:
|
||||
envelope.type === EnvelopeType.DOCUMENT ? DocumentSource.DOCUMENT : DocumentSource.TEMPLATE,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Key = original envelope item ID
|
||||
// Value = duplicated envelope item ID.
|
||||
const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};
|
||||
|
||||
// Duplicate the envelope items.
|
||||
await Promise.all(
|
||||
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||
const duplicatedDocumentData = await prisma.documentData.create({
|
||||
data: {
|
||||
type: envelopeItem.documentData.type,
|
||||
data: envelopeItem.documentData.initialData,
|
||||
initialData: envelopeItem.documentData.initialData,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicatedEnvelopeItem = await prisma.envelopeItem.create({
|
||||
data: {
|
||||
id: prefixedId('envelope_item'),
|
||||
title: envelopeItem.title,
|
||||
order: envelopeItem.order,
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
documentDataId: duplicatedDocumentData.id,
|
||||
},
|
||||
});
|
||||
|
||||
oldEnvelopeItemToNewEnvelopeItemIdMap[envelopeItem.id] = duplicatedEnvelopeItem.id;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const recipient of envelope.recipients) {
|
||||
await prisma.recipient.create({
|
||||
data: {
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
fields: {
|
||||
createMany: {
|
||||
data: recipient.fields.map((field) => ({
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicatedEnvelope.type === EnvelopeType.DOCUMENT) {
|
||||
const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: duplicatedEnvelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)),
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: duplicatedEnvelope.id,
|
||||
envelope: duplicatedEnvelope,
|
||||
legacyId: {
|
||||
type: envelope.type,
|
||||
id: legacyNumberId,
|
||||
},
|
||||
};
|
||||
};
|
||||
199
packages/lib/server-only/envelope/get-envelope-by-id.ts
Normal file
199
packages/lib/server-only/envelope/get-envelope-by-id.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetEnvelopeByIdOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
/**
|
||||
* The validated team ID.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unvalidated team ID.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
export const getEnvelopeById = async ({ id, userId, teamId, type }: GetEnvelopeByIdOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
folder: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...envelope,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type GetEnvelopeByIdResponse = Awaited<ReturnType<typeof getEnvelopeById>>;
|
||||
|
||||
export type GetEnvelopeWhereInputOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
/**
|
||||
* The user ID who has been authenticated.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The unknown teamId from the request.
|
||||
*/
|
||||
teamId: number;
|
||||
|
||||
/**
|
||||
* The type of envelope to get.
|
||||
*
|
||||
* Set to null to bypass check.
|
||||
*/
|
||||
type: EnvelopeType | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a given Prisma envelope query.
|
||||
*
|
||||
* This will return a query that allows a user to get a document if they have valid access to it.
|
||||
*
|
||||
* NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes.
|
||||
*/
|
||||
export const getEnvelopeWhereInput = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
}: GetEnvelopeWhereInputOptions) => {
|
||||
// Backup validation incase something goes wrong.
|
||||
if (!id.id || !userId || !teamId || type === undefined) {
|
||||
console.error(`[CRTICAL ERROR]: MUST NEVER HAPPEN`);
|
||||
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope ID not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that the user belongs to the team provided.
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
const envelopeOrInput: Prisma.EnvelopeWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent from the team email.
|
||||
if (team.teamEmail) {
|
||||
envelopeOrInput.push({
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
// NOTE: DO NOT PUT ANY CODE AFTER THIS POINT.
|
||||
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
|
||||
const envelopeWhereInput: Prisma.EnvelopeWhereUniqueInput = {
|
||||
...unsafeBuildEnvelopeIdQuery(id, type),
|
||||
OR: envelopeOrInput,
|
||||
};
|
||||
|
||||
// Final backup validation incase something goes wrong.
|
||||
if (
|
||||
!envelopeWhereInput.OR ||
|
||||
envelopeWhereInput.OR.length < 2 ||
|
||||
!userId ||
|
||||
!teamId ||
|
||||
!team.id ||
|
||||
teamId !== team.id
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Query not valid',
|
||||
});
|
||||
}
|
||||
|
||||
// Do not modify this return directly, all adjustments need to be made prior to the above if statement.
|
||||
return {
|
||||
envelopeWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,311 @@
|
||||
import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
|
||||
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { ZFieldSchema } from '../../types/field';
|
||||
import { ZRecipientLiteSchema } from '../../types/recipient';
|
||||
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export type GetRecipientEnvelopeByTokenOptions = {
|
||||
token: string;
|
||||
userId?: number;
|
||||
accessAuth?: TDocumentAuthMethods;
|
||||
};
|
||||
|
||||
const ZEnvelopeForSigningResponse = z.object({
|
||||
envelope: EnvelopeSchema.pick({
|
||||
type: true,
|
||||
status: true,
|
||||
id: true,
|
||||
secondaryId: true,
|
||||
internalVersion: true,
|
||||
completedAt: true,
|
||||
deletedAt: true,
|
||||
title: true,
|
||||
authOptions: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
}).extend({
|
||||
documentMeta: DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
timezone: true,
|
||||
dateFormat: true,
|
||||
redirectUrl: true,
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
language: true,
|
||||
}),
|
||||
recipients: ZRecipientLiteSchema.pick({
|
||||
id: true,
|
||||
role: true,
|
||||
signingStatus: true,
|
||||
email: true,
|
||||
name: true,
|
||||
documentDeletedAt: true,
|
||||
expired: true,
|
||||
signedAt: true,
|
||||
authOptions: true,
|
||||
signingOrder: true,
|
||||
rejectionReason: true,
|
||||
})
|
||||
.extend({
|
||||
fields: ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
}).array(),
|
||||
})
|
||||
.array(),
|
||||
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
title: true,
|
||||
documentDataId: true,
|
||||
order: true,
|
||||
})
|
||||
.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
}),
|
||||
user: UserSchema.pick({
|
||||
name: true,
|
||||
email: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
/**
|
||||
* The recipient that is currently signing.
|
||||
*/
|
||||
recipient: ZRecipientLiteSchema.pick({
|
||||
id: true,
|
||||
role: true,
|
||||
envelopeId: true,
|
||||
readStatus: true,
|
||||
sendStatus: true,
|
||||
signingStatus: true,
|
||||
email: true,
|
||||
name: true,
|
||||
documentDeletedAt: true,
|
||||
expired: true,
|
||||
signedAt: true,
|
||||
authOptions: true,
|
||||
token: true,
|
||||
signingOrder: true,
|
||||
rejectionReason: true,
|
||||
}).extend({
|
||||
fields: ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
})
|
||||
.extend({
|
||||
signature: SignatureSchema.nullish(),
|
||||
})
|
||||
.array(),
|
||||
}),
|
||||
recipientSignature: SignatureSchema.pick({
|
||||
signatureImageAsBase64: true,
|
||||
typedSignature: true,
|
||||
}).nullable(),
|
||||
|
||||
isCompleted: z.boolean(),
|
||||
isRejected: z.boolean(),
|
||||
isRecipientsTurn: z.boolean(),
|
||||
|
||||
sender: z.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
|
||||
settings: z.object({
|
||||
includeSenderDetails: z.boolean(),
|
||||
brandingEnabled: z.boolean(),
|
||||
brandingLogo: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type EnvelopeForSigningResponse = z.infer<typeof ZEnvelopeForSigningResponse>;
|
||||
|
||||
/**
|
||||
* Get all the values and details for an envelope that a recipient requires
|
||||
* to sign an envelope.
|
||||
*
|
||||
* Do not overexpose any information that the recipient should not have.
|
||||
*/
|
||||
export const getEnvelopeForRecipientSigning = async ({
|
||||
token,
|
||||
userId,
|
||||
accessAuth,
|
||||
}: GetRecipientEnvelopeByTokenOptions): Promise<EnvelopeForSigningResponse> => {
|
||||
if (!token) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Missing token',
|
||||
});
|
||||
}
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
recipients: {
|
||||
include: {
|
||||
fields: {
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
signingOrder: 'asc',
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
teamEmail: true,
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSigningCertificate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = (envelope?.recipients || []).find((r) => r.token === token);
|
||||
|
||||
if (!envelope || !recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.envelopeItems.length === 0) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope has no items',
|
||||
});
|
||||
}
|
||||
|
||||
const documentAccessValid = await isRecipientAuthorized({
|
||||
type: 'ACCESS',
|
||||
documentAuthOptions: envelope.authOptions,
|
||||
recipient,
|
||||
userId,
|
||||
authOptions: accessAuth,
|
||||
});
|
||||
|
||||
if (!documentAccessValid) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Invalid access values',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({ teamId: envelope.teamId });
|
||||
|
||||
// Get the signature if they have put it in already.
|
||||
const recipientSignature = await prisma.signature.findFirst({
|
||||
where: {
|
||||
field: {
|
||||
recipientId: recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
recipientId: true,
|
||||
signatureImageAsBase64: true,
|
||||
typedSignature: true,
|
||||
},
|
||||
});
|
||||
|
||||
let isRecipientsTurn = true;
|
||||
|
||||
const currentRecipientIndex = envelope.recipients.findIndex((r) => r.token === token);
|
||||
|
||||
if (
|
||||
envelope.documentMeta.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||
currentRecipientIndex !== -1
|
||||
) {
|
||||
for (let i = 0; i < currentRecipientIndex; i++) {
|
||||
if (envelope.recipients[i].signingStatus !== SigningStatus.SIGNED) {
|
||||
isRecipientsTurn = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sender = settings.includeSenderDetails
|
||||
? {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
}
|
||||
: {
|
||||
email: envelope.team.teamEmail?.email || '',
|
||||
name: envelope.team.name || '',
|
||||
};
|
||||
|
||||
return ZEnvelopeForSigningResponse.parse({
|
||||
envelope,
|
||||
recipient,
|
||||
recipientSignature,
|
||||
isRecipientsTurn,
|
||||
isCompleted:
|
||||
recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
envelope.status === DocumentStatus.COMPLETED,
|
||||
isRejected:
|
||||
recipient.signingStatus === SigningStatus.REJECTED ||
|
||||
envelope.status === DocumentStatus.REJECTED,
|
||||
sender,
|
||||
settings: {
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
brandingEnabled: settings.brandingEnabled,
|
||||
brandingLogo: settings.brandingLogo,
|
||||
},
|
||||
} satisfies EnvelopeForSigningResponse);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export const getEnvelopeRequiredAccessData = async ({ token }: { token: string }) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = envelope.recipients.find((r) => r.token === token);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientUserAccount = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: recipient.email.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
recipientEmail: recipient.email,
|
||||
recipientHasAccount: Boolean(recipientUserAccount),
|
||||
} as const;
|
||||
};
|
||||
39
packages/lib/server-only/envelope/increment-id.ts
Normal file
39
packages/lib/server-only/envelope/increment-id.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { mapDocumentIdToSecondaryId, mapTemplateIdToSecondaryId } from '../../utils/envelope';
|
||||
|
||||
export const incrementDocumentId = async () => {
|
||||
const documentIdCounter = await prisma.counter.update({
|
||||
where: {
|
||||
id: 'document',
|
||||
},
|
||||
data: {
|
||||
value: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
documentId: documentIdCounter.value,
|
||||
formattedDocumentId: mapDocumentIdToSecondaryId(documentIdCounter.value),
|
||||
};
|
||||
};
|
||||
|
||||
export const incrementTemplateId = async () => {
|
||||
const templateIdCounter = await prisma.counter.update({
|
||||
where: {
|
||||
id: 'template',
|
||||
},
|
||||
data: {
|
||||
value: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
templateId: templateIdCounter.value,
|
||||
formattedTemplateId: mapTemplateIdToSecondaryId(templateIdCounter.value),
|
||||
};
|
||||
};
|
||||
344
packages/lib/server-only/envelope/update-envelope.ts
Normal file
344
packages/lib/server-only/envelope/update-envelope.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client';
|
||||
import { EnvelopeType, FolderType } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from './get-envelope-by-id';
|
||||
|
||||
export type UpdateEnvelopeOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
data?: {
|
||||
title?: string;
|
||||
folderId?: string | null;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
publicTitle?: string;
|
||||
publicDescription?: string;
|
||||
templateType?: TemplateType;
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const updateEnvelope = async ({
|
||||
userId,
|
||||
teamId,
|
||||
id,
|
||||
data = {},
|
||||
meta = {},
|
||||
requestMetadata,
|
||||
}: UpdateEnvelopeOptions) => {
|
||||
const { envelopeWhereInput, team } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null, // Allow updating both documents and templates.
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
organisationId: true,
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
envelope.type !== EnvelopeType.TEMPLATE &&
|
||||
(data.publicTitle || data.publicDescription || data.templateType)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the template fields for document type envelopes',
|
||||
});
|
||||
}
|
||||
|
||||
// If no data just return the document since this function is normally chained after a meta update.
|
||||
if (Object.values(data).length === 0 && Object.keys(meta).length === 0) {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
const isEnvelopeOwner = envelope.userId === userId;
|
||||
|
||||
// Validate whether the new visibility setting is allowed for the current user.
|
||||
if (
|
||||
!isEnvelopeOwner &&
|
||||
data?.visibility &&
|
||||
!canAccessTeamDocument(team.currentTeamRole, data.visibility)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update the envelope visibility',
|
||||
});
|
||||
}
|
||||
|
||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||
documentAuth: envelope.authOptions,
|
||||
});
|
||||
|
||||
const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null;
|
||||
const documentGlobalActionAuth = documentAuthOption?.globalActionAuth ?? null;
|
||||
|
||||
// If the new global auth values aren't passed in, fallback to the current document values.
|
||||
const newGlobalAccessAuth =
|
||||
data?.globalAccessAuth === undefined ? documentGlobalAccessAuth : data.globalAccessAuth;
|
||||
const newGlobalActionAuth =
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: newGlobalAccessAuth,
|
||||
globalActionAuth: newGlobalActionAuth,
|
||||
});
|
||||
|
||||
const emailId = meta.emailId;
|
||||
|
||||
// Validate the emailId belongs to the organisation.
|
||||
if (emailId) {
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisationId: envelope.team.organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined;
|
||||
|
||||
// Validate folder ID.
|
||||
if (data.folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: data.folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
type: envelope.type === EnvelopeType.TEMPLATE ? FolderType.TEMPLATE : FolderType.DOCUMENT,
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
|
||||
folderUpdateQuery = {
|
||||
connect: {
|
||||
id: data.folderId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Move to root folder if folderId is null.
|
||||
if (data.folderId === null) {
|
||||
folderUpdateQuery = {
|
||||
disconnect: true,
|
||||
};
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === envelope.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === envelope.externalId;
|
||||
const isGlobalAccessSame =
|
||||
documentGlobalAccessAuth === undefined ||
|
||||
isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
|
||||
const isGlobalActionSame =
|
||||
documentGlobalActionAuth === undefined ||
|
||||
isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === envelope.visibility;
|
||||
const isFolderSame = data.folderId === undefined || data.folderId === envelope.folderId;
|
||||
const isTemplateTypeSame =
|
||||
data.templateType === undefined || data.templateType === envelope.templateType;
|
||||
const isPublicDescriptionSame =
|
||||
data.publicDescription === undefined || data.publicDescription === envelope.publicDescription;
|
||||
const isPublicTitleSame =
|
||||
data.publicTitle === undefined || data.publicTitle === envelope.publicTitle;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
if (!isTitleSame && envelope.status !== DocumentStatus.DRAFT) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You cannot update the title if the envelope has been sent',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTitleSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: envelope.title,
|
||||
to: data.title || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalIdSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: envelope.externalId,
|
||||
to: data.externalId || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalAccessSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalAccessAuth,
|
||||
to: newGlobalAccessAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isGlobalActionSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: documentGlobalActionAuth,
|
||||
to: newGlobalActionAuth,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDocumentVisibilitySame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: envelope.visibility,
|
||||
to: data.visibility || '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Todo: Decide if we want to log moving the document around.
|
||||
// if (!isFolderSame) {
|
||||
// auditLogs.push(
|
||||
// createDocumentAuditLogData({
|
||||
// type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FOLDER_UPDATED,
|
||||
// envelopeId: envelope.id,
|
||||
// metadata: requestMetadata,
|
||||
// data: {
|
||||
// from: envelope.folderId,
|
||||
// to: data.folderId || '',
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Todo: Determine if changes are made
|
||||
// Commented out since we didn't detect the changes to sequence.
|
||||
// const isMetaSame = isDeepEqual(envelope.documentMeta, meta);
|
||||
// Early return if nothing is required.
|
||||
// if (
|
||||
// auditLogs.length === 0 &&
|
||||
// data.useLegacyFieldInsertion === undefined &&
|
||||
// isFolderSame &&
|
||||
// isTemplateTypeSame &&
|
||||
// isPublicDescriptionSame &&
|
||||
// isPublicTitleSame
|
||||
// ) {
|
||||
// return envelope;
|
||||
// }
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const updatedEnvelope = await tx.envelope.update({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility,
|
||||
templateType: data.templateType,
|
||||
publicDescription: data.publicDescription,
|
||||
publicTitle: data.publicTitle,
|
||||
useLegacyFieldInsertion: data.useLegacyFieldInsertion,
|
||||
authOptions,
|
||||
folder: folderUpdateQuery,
|
||||
documentMeta: {
|
||||
update: {
|
||||
...meta,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: auditLogs,
|
||||
});
|
||||
}
|
||||
|
||||
return updatedEnvelope;
|
||||
});
|
||||
};
|
||||
@ -1,126 +0,0 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export interface CreateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
fields: (TFieldAndMeta & {
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
})[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createDocumentFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: CreateDocumentFieldsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
validatedFields.map(async (field) => {
|
||||
const createdField = await tx.field.create({
|
||||
data: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
documentId,
|
||||
recipientId: field.recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle field created audit log.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: createdField.secondaryId,
|
||||
fieldRecipientEmail: field.recipientEmail,
|
||||
fieldRecipientId: createdField.recipientId,
|
||||
fieldType: createdField.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return createdField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
168
packages/lib/server-only/field/create-envelope-fields.ts
Normal file
168
packages/lib/server-only/field/create-envelope-fields.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateEnvelopeFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
fields: (TFieldAndMeta & {
|
||||
/**
|
||||
* The ID of the item to insert the fields into.
|
||||
*
|
||||
* If blank, the first item will be used.
|
||||
*/
|
||||
envelopeItemId?: string;
|
||||
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
})[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createEnvelopeFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
id,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: CreateEnvelopeFieldsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: null, // Null to allow any type of envelope.
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.type === EnvelopeType.DOCUMENT && envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const firstEnvelopeItem = envelope.envelopeItems[0];
|
||||
|
||||
if (!firstEnvelopeItem) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// The item to attach the fields to MUST belong to the document.
|
||||
if (
|
||||
field.envelopeItemId &&
|
||||
!envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === field.envelopeItemId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Item to attach fields to must belong to the document',
|
||||
});
|
||||
}
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
envelopeItemId: field.envelopeItemId || firstEnvelopeItem.id, // Fallback to first envelope item if no envelope item ID is provided.
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
const newlyCreatedFields = await tx.field.createManyAndReturn({
|
||||
data: validatedFields.map((field) => ({
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
recipientId: field.recipientId,
|
||||
})),
|
||||
});
|
||||
|
||||
// Handle field created audit log.
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.createMany({
|
||||
data: newlyCreatedFields.map((createdField) => {
|
||||
const recipient = validatedFields.find(
|
||||
(field) => field.recipientId === createdField.recipientId,
|
||||
);
|
||||
|
||||
return createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: createdField.secondaryId,
|
||||
fieldRecipientEmail: recipient?.recipientEmail || '',
|
||||
fieldRecipientId: createdField.recipientId,
|
||||
fieldType: createdField.type,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return newlyCreatedFields;
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
};
|
||||
};
|
||||
@ -1,136 +0,0 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
|
||||
export type CreateFieldOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fieldMeta?: FieldMeta;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const createField = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
fieldMeta,
|
||||
requestMetadata,
|
||||
}: CreateFieldOptions) => {
|
||||
const { documentWhereInput, team } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type);
|
||||
|
||||
if (advancedField && !fieldMeta) {
|
||||
throw new Error(
|
||||
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
|
||||
throw new Error('Field meta type does not match the field type');
|
||||
}
|
||||
|
||||
const result = match(type)
|
||||
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
|
||||
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
|
||||
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
|
||||
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
|
||||
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
|
||||
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
|
||||
success: true,
|
||||
data: {},
|
||||
}))
|
||||
.with('FREE_SIGNATURE', () => ({
|
||||
success: false,
|
||||
error: 'FREE_SIGNATURE is not supported',
|
||||
data: {},
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Field meta parsing failed');
|
||||
}
|
||||
|
||||
const field = await prisma.field.create({
|
||||
data: {
|
||||
documentId,
|
||||
recipientId,
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: result.data,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_CREATED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team.id,
|
||||
email: team.name,
|
||||
name: '',
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -1,101 +0,0 @@
|
||||
import 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 { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface CreateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const createTemplateFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: CreateTemplateFieldsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'template not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Field validation.
|
||||
const validatedFields = fields.map((field) => {
|
||||
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient ${field.recipientId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can have new fields created.
|
||||
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Recipient type cannot have fields, or they have already interacted with the template.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
recipientEmail: recipient.email,
|
||||
};
|
||||
});
|
||||
|
||||
const createdFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
validatedFields.map(async (field) => {
|
||||
const createdField = await tx.field.create({
|
||||
data: {
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
templateId,
|
||||
recipientId: field.recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
return createdField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: createdFields,
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
@ -5,7 +7,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface DeleteDocumentFieldOptions {
|
||||
userId: number;
|
||||
@ -19,7 +21,8 @@ export const deleteDocumentField = async ({
|
||||
teamId,
|
||||
fieldId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentFieldOptions): Promise<void> => {
|
||||
}: DeleteDocumentFieldOptions) => {
|
||||
// Unauthenticated check, we do the real check later.
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
@ -32,22 +35,18 @@ export const deleteDocumentField = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const documentId = field.documentId;
|
||||
|
||||
if (!documentId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field does not belong to a document. Use delete template field instead.',
|
||||
});
|
||||
}
|
||||
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
@ -60,19 +59,19 @@ export const deleteDocumentField = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
@ -87,10 +86,11 @@ export const deleteDocumentField = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const deletedField = await tx.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -98,7 +98,7 @@ export const deleteDocumentField = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: deletedField.secondaryId,
|
||||
@ -108,5 +108,7 @@ export const deleteDocumentField = async ({
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return deletedField;
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import type { Team } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteField = async ({
|
||||
fieldId,
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
}: DeleteFieldOptions) => {
|
||||
const field = await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_DELETED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.recipient?.email ?? '',
|
||||
fieldRecipientId: field.recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -1,7 +1,10 @@
|
||||
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 DeleteTemplateFieldOptions {
|
||||
userId: number;
|
||||
@ -17,21 +20,34 @@ export const deleteTemplateField = async ({
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
template: {
|
||||
envelope: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!field || !field.templateId) {
|
||||
if (!field) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Additional validation to check visibility.
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
id: field.id,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -6,10 +6,12 @@ export type GetCompletedFieldsForTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
// Note: You many need to filter this on a per envelope item ID basis.
|
||||
export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
document: {
|
||||
envelope: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
|
||||
@ -1,53 +1,61 @@
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetFieldByIdOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
fieldId: number;
|
||||
envelopeType: EnvelopeType;
|
||||
};
|
||||
|
||||
export const getFieldById = async ({ userId, teamId, fieldId }: GetFieldByIdOptions) => {
|
||||
export const getFieldById = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
envelopeType,
|
||||
}: GetFieldByIdOptions) => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
template: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
envelope: {
|
||||
type: envelopeType,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const foundTeamId = field?.document?.teamId || field?.template?.teamId;
|
||||
|
||||
if (!field || !foundTeamId || foundTeamId !== teamId) {
|
||||
if (!field) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId: foundTeamId,
|
||||
userId,
|
||||
}),
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: envelopeType,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
// Additional validation to check visibility.
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: envelopeWhereInput,
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Field not found',
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
return mapFieldToLegacyField(field, envelope);
|
||||
};
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface GetFieldsForDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||
|
||||
export const getFieldsForDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetFieldsForDocumentOptions) => {
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -6,6 +6,7 @@ export type GetFieldsForTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
// Note: You many need to filter this on a per envelope item ID basis.
|
||||
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
|
||||
if (!token) {
|
||||
throw new Error('Missing token');
|
||||
@ -35,7 +36,10 @@ export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) =>
|
||||
gte: recipient.signingOrder ?? 0,
|
||||
},
|
||||
},
|
||||
documentId: recipient.documentId,
|
||||
envelope: {
|
||||
id: recipient.envelopeId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
{
|
||||
recipientId: recipient.id,
|
||||
|
||||
@ -41,19 +41,19 @@ export const removeSignedFieldWithToken = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
envelope: true,
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { document } = field;
|
||||
const { envelope } = field;
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (document.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${envelope.id} must be pending`);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -89,7 +89,7 @@ export const removeSignedFieldWithToken = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { Field } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, type Field, FieldType } from '@prisma/client';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
@ -25,13 +24,15 @@ import {
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
fields: FieldData[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
@ -39,43 +40,47 @@ export interface SetFieldsForDocumentOptions {
|
||||
export const setFieldsForDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
id,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: SetFieldsForDocumentOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const existingFields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
const existingFields = envelope.fields;
|
||||
|
||||
const removedFields = existingFields.filter(
|
||||
(existingField) => !fields.find((field) => field.id === existingField.id),
|
||||
@ -84,7 +89,18 @@ export const setFieldsForDocument = async ({
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Check whether the field is being attached to an allowed envelope item.
|
||||
const foundEnvelopeItem = envelope.envelopeItems.find(
|
||||
(envelopeItem) => envelopeItem.id === field.envelopeItemId,
|
||||
);
|
||||
|
||||
if (!foundEnvelopeItem) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Envelope item ${field.envelopeItemId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
@ -105,6 +121,14 @@ export const setFieldsForDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent creating new fields when recipient has interacted with the document.
|
||||
if (!existing && !canRecipientFieldsBeModified(recipient, existingFields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
_persisted: existing,
|
||||
@ -115,7 +139,7 @@ export const setFieldsForDocument = async ({
|
||||
const persistedFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
linkedFields.map(async (field) => {
|
||||
const fieldSignerEmail = field.signerEmail.toLowerCase();
|
||||
const fieldSignerEmail = field._recipient.email.toLowerCase();
|
||||
|
||||
const parsedFieldMeta = field.fieldMeta
|
||||
? ZFieldMetaSchema.parse(field.fieldMeta)
|
||||
@ -197,7 +221,8 @@ export const setFieldsForDocument = async ({
|
||||
const upsertedField = await tx.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
},
|
||||
update: {
|
||||
page: field.pageNumber,
|
||||
@ -217,15 +242,21 @@ export const setFieldsForDocument = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
document: {
|
||||
envelope: {
|
||||
connect: {
|
||||
id: documentId,
|
||||
id: envelope.id,
|
||||
},
|
||||
},
|
||||
envelopeItem: {
|
||||
connect: {
|
||||
id: field.envelopeItemId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
id: field.recipientId,
|
||||
documentId,
|
||||
id: field._recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -249,7 +280,7 @@ export const setFieldsForDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
@ -264,7 +295,7 @@ export const setFieldsForDocument = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
...baseAuditLog,
|
||||
@ -292,7 +323,7 @@ export const setFieldsForDocument = async ({
|
||||
data: removedFields.map((field) =>
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
@ -315,7 +346,9 @@ export const setFieldsForDocument = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
fields: [...filteredFields, ...persistedFields],
|
||||
fields: [...filteredFields, ...persistedFields].map((field) =>
|
||||
mapFieldToLegacyField(field, envelope),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -324,8 +357,8 @@ export const setFieldsForDocument = async ({
|
||||
*/
|
||||
type FieldData = {
|
||||
id?: number | null;
|
||||
envelopeItemId: string;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
@ -340,6 +373,7 @@ const hasFieldBeenChanged = (field: Field, newFieldData: FieldData) => {
|
||||
const newFieldMeta = newFieldData.fieldMeta || null;
|
||||
|
||||
return (
|
||||
field.envelopeItemId !== newFieldData.envelopeItemId ||
|
||||
field.type !== newFieldData.type ||
|
||||
field.page !== newFieldData.pageNumber ||
|
||||
field.positionX.toNumber() !== newFieldData.pageX ||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, FieldType } from '@prisma/client';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
@ -16,16 +16,19 @@ import {
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type SetFieldsForTemplateOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
fields: {
|
||||
id?: number | null;
|
||||
envelopeItemId: string;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
@ -39,28 +42,40 @@ export type SetFieldsForTemplateOptions = {
|
||||
export const setFieldsForTemplate = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
id,
|
||||
fields,
|
||||
}: SetFieldsForTemplateOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Template not found');
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const existingFields = await prisma.field.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
const existingFields = envelope.fields;
|
||||
|
||||
const removedFields = existingFields.filter(
|
||||
(existingField) => !fields.find((field) => field.id === existingField.id),
|
||||
@ -69,9 +84,30 @@ export const setFieldsForTemplate = async ({
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Check whether the field is being attached to an allowed envelope item.
|
||||
const foundEnvelopeItem = envelope.envelopeItems.find(
|
||||
(envelopeItem) => envelopeItem.id === field.envelopeItemId,
|
||||
);
|
||||
|
||||
if (!foundEnvelopeItem) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Envelope item ${field.envelopeItemId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient not found for field ${field.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
_persisted: existing,
|
||||
_recipient: recipient,
|
||||
};
|
||||
});
|
||||
|
||||
@ -143,7 +179,8 @@ export const setFieldsForTemplate = async ({
|
||||
return prisma.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
templateId,
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
},
|
||||
update: {
|
||||
page: field.pageNumber,
|
||||
@ -163,15 +200,21 @@ export const setFieldsForTemplate = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
template: {
|
||||
envelope: {
|
||||
connect: {
|
||||
id: templateId,
|
||||
id: envelope.id,
|
||||
},
|
||||
},
|
||||
envelopeItem: {
|
||||
connect: {
|
||||
id: field.envelopeItemId,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
id: field.recipientId,
|
||||
templateId,
|
||||
id: field._recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -198,6 +241,8 @@ export const setFieldsForTemplate = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
fields: [...filteredFields, ...persistedFields],
|
||||
fields: [...filteredFields, ...persistedFields].map((field) =>
|
||||
mapFieldToLegacyField(field, envelope),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -83,7 +83,7 @@ export const signFieldWithToken = async ({
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
envelope: {
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
@ -92,9 +92,9 @@ export const signFieldWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const { document } = field;
|
||||
const { envelope } = field;
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
@ -102,12 +102,12 @@ export const signFieldWithToken = async ({
|
||||
throw new Error(`Recipient not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
if (document.deletedAt) {
|
||||
throw new Error(`Document ${document.id} has been deleted`);
|
||||
if (envelope.deletedAt) {
|
||||
throw new Error(`Document ${envelope.id} has been deleted`);
|
||||
}
|
||||
|
||||
if (document.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${document.id} must be pending for signing`);
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new Error(`Document ${envelope.id} must be pending for signing`);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -179,7 +179,7 @@ export const signFieldWithToken = async ({
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
documentAuthOptions: envelope.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
@ -188,7 +188,9 @@ export const signFieldWithToken = async ({
|
||||
|
||||
const documentMeta = await prisma.documentMeta.findFirst({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
envelope: {
|
||||
id: envelope.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -279,7 +281,7 @@ export const signFieldWithToken = async ({
|
||||
assistant && field.recipientId !== assistant.id
|
||||
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
|
||||
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
email: assistant?.email ?? recipient.email,
|
||||
name: assistant?.name ?? recipient.name,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import { EnvelopeType, type FieldType } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
@ -10,8 +10,9 @@ import {
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
@ -37,34 +38,38 @@ export const updateDocumentFields = async ({
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentFieldsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = document.fields.find((existingField) => existingField.id === field.id);
|
||||
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -72,7 +77,7 @@ export const updateDocumentFields = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find(
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
@ -84,7 +89,7 @@ export const updateDocumentFields = async ({
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, document.fields)) {
|
||||
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',
|
||||
@ -123,7 +128,7 @@ export const updateDocumentFields = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
@ -142,6 +147,6 @@ export const updateDocumentFields = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
fields: updatedFields,
|
||||
fields: updatedFields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import type { FieldType, Team } from '@prisma/client';
|
||||
|
||||
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData, diffFieldChanges } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdateFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipientId?: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
pageWidth?: number;
|
||||
pageHeight?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
fieldMeta?: FieldMeta;
|
||||
};
|
||||
|
||||
export const updateField = async ({
|
||||
fieldId,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata,
|
||||
fieldMeta,
|
||||
}: UpdateFieldOptions) => {
|
||||
if (type === 'FREE_SIGNATURE') {
|
||||
throw new Error('Cannot update a FREE_SIGNATURE field');
|
||||
}
|
||||
|
||||
const oldField = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
document: {
|
||||
id: documentId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const field = prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
id: fieldId,
|
||||
},
|
||||
data: {
|
||||
recipientId,
|
||||
type,
|
||||
page: pageNumber,
|
||||
positionX: pageX,
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
fieldMeta,
|
||||
},
|
||||
include: {
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
}
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
fieldRecipientEmail: updatedField.recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId ?? -1,
|
||||
fieldType: updatedField.type,
|
||||
changes: diffFieldChanges(oldField, updatedField),
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return updatedField;
|
||||
});
|
||||
|
||||
return field;
|
||||
};
|
||||
@ -1,11 +1,12 @@
|
||||
import type { FieldType } from '@prisma/client';
|
||||
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 { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
@ -29,25 +30,32 @@ export const updateTemplateFields = async ({
|
||||
templateId,
|
||||
fields,
|
||||
}: UpdateTemplateFieldsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = template.fields.find((existingField) => existingField.id === field.id);
|
||||
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -55,7 +63,7 @@ export const updateTemplateFields = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = template.recipients.find(
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
@ -67,7 +75,7 @@ export const updateTemplateFields = async ({
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, template.fields)) {
|
||||
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',
|
||||
@ -103,6 +111,6 @@ export const updateTemplateFields = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
fields: updatedFields,
|
||||
fields: updatedFields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,8 +2,6 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { FolderType } from '../../types/folder-type';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
|
||||
export interface CreateFolderOptions {
|
||||
@ -21,8 +19,7 @@ export const createFolder = async ({
|
||||
parentId,
|
||||
type = FolderType.DOCUMENT,
|
||||
}: CreateFolderOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
// This indirectly verifies whether the user has access to the team.
|
||||
const settings = await getTeamSettings({ userId, teamId });
|
||||
|
||||
return await prisma.folder.create({
|
||||
@ -32,7 +29,7 @@ export const createFolder = async ({
|
||||
teamId,
|
||||
parentId,
|
||||
type,
|
||||
visibility: determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole),
|
||||
visibility: settings.documentVisibility,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface DeleteFolderOptions {
|
||||
@ -24,11 +21,6 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
documents: true,
|
||||
subfolders: true,
|
||||
templates: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
@ -37,11 +29,7 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
|
||||
});
|
||||
}
|
||||
|
||||
const hasPermission = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => true)
|
||||
.with(TeamMemberRole.MANAGER, () => folder.visibility !== DocumentVisibility.ADMIN)
|
||||
.with(TeamMemberRole.MEMBER, () => folder.visibility === DocumentVisibility.EVERYONE)
|
||||
.otherwise(() => false);
|
||||
const hasPermission = canAccessTeamDocument(team.currentTeamRole, folder.visibility);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import type { TFolderType } from '../../types/folder-type';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
@ -17,22 +16,11 @@ export interface FindFoldersOptions {
|
||||
export const findFolders = async ({ userId, teamId, parentId, type }: FindFoldersOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE }));
|
||||
const visibilityFilters = {
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
};
|
||||
|
||||
const whereClause = {
|
||||
AND: [
|
||||
@ -69,13 +57,15 @@ export const findFolders = async ({ userId, teamId, parentId, type }: FindFolder
|
||||
createdAt: 'desc',
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
folderId: folder.id,
|
||||
},
|
||||
}),
|
||||
prisma.template.count({
|
||||
prisma.envelope.count({
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
folderId: folder.id,
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export interface MoveDocumentToFolderOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
folderId?: string | null;
|
||||
requestMetadata?: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const moveDocumentToFolder = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
folderId,
|
||||
}: MoveDocumentToFolderOptions) => {
|
||||
const team = await getTeamById({ userId, teamId });
|
||||
|
||||
const visibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => ({
|
||||
visibility: {
|
||||
in: [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
],
|
||||
},
|
||||
}))
|
||||
.with(TeamMemberRole.MANAGER, () => ({
|
||||
visibility: {
|
||||
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
|
||||
},
|
||||
}))
|
||||
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE }));
|
||||
|
||||
const documentWhereClause = {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereClause,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
const folderWhereClause = {
|
||||
id: folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
OR: [
|
||||
{ teamId, ...visibilityFilters },
|
||||
{ userId, teamId },
|
||||
],
|
||||
};
|
||||
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: folderWhereClause,
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,63 +0,0 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export interface MoveTemplateToFolderOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
templateId: number;
|
||||
folderId?: string | null;
|
||||
}
|
||||
|
||||
export const moveTemplateToFolder = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
folderId,
|
||||
}: MoveTemplateToFolderOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId !== null) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folderId,
|
||||
team: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
}),
|
||||
type: FolderType.TEMPLATE,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.template.update({
|
||||
where: {
|
||||
id: templateId,
|
||||
},
|
||||
data: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PDFDocument } from '@cantoo/pdf-lib';
|
||||
import { TextAlignment, rgb, setFontAndSize } from '@cantoo/pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import type { PDFDocument } from 'pdf-lib';
|
||||
import { TextAlignment, rgb, setFontAndSize } from 'pdf-lib';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { getPageSize } from './get-page-size';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFAnnotation, PDFRef } from 'pdf-lib';
|
||||
import { PDFAnnotation, PDFRef } from '@cantoo/pdf-lib';
|
||||
import {
|
||||
PDFDict,
|
||||
type PDFDocument,
|
||||
@ -8,7 +8,7 @@ import {
|
||||
pushGraphicsState,
|
||||
rotateInPlace,
|
||||
translate,
|
||||
} from 'pdf-lib';
|
||||
} from '@cantoo/pdf-lib';
|
||||
|
||||
export const flattenAnnotations = (document: PDFDocument) => {
|
||||
const pages = document.getPages();
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
|
||||
import type { PDFField, PDFWidgetAnnotation } from '@cantoo/pdf-lib';
|
||||
import {
|
||||
PDFCheckBox,
|
||||
PDFDict,
|
||||
@ -12,7 +11,8 @@ import {
|
||||
pushGraphicsState,
|
||||
rotateInPlace,
|
||||
translate,
|
||||
} from 'pdf-lib';
|
||||
} from '@cantoo/pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PDFPage } from 'pdf-lib';
|
||||
import type { PDFPage } from '@cantoo/pdf-lib';
|
||||
|
||||
/**
|
||||
* Gets the effective page size for PDF operations.
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import type { PDFDocument, PDFFont, PDFTextField } from 'pdf-lib';
|
||||
import type { PDFDocument, PDFFont, PDFTextField } from '@cantoo/pdf-lib';
|
||||
import {
|
||||
RotationTypes,
|
||||
TextAlignment,
|
||||
@ -9,7 +7,9 @@ import {
|
||||
radiansToDegrees,
|
||||
rgb,
|
||||
setFontAndSize,
|
||||
} from 'pdf-lib';
|
||||
} from '@cantoo/pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
@ -35,7 +35,7 @@ import {
|
||||
} from '../../types/field-meta';
|
||||
import { getPageSize } from './get-page-size';
|
||||
|
||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
const [fontCaveat, fontNoto] = await Promise.all([
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
||||
133
packages/lib/server-only/pdf/insert-field-in-pdf-v2.ts
Normal file
133
packages/lib/server-only/pdf/insert-field-in-pdf-v2.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import type { PDFDocument } from '@cantoo/pdf-lib';
|
||||
import { RotationTypes, radiansToDegrees } from '@cantoo/pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import Konva from 'konva';
|
||||
import 'konva/skia-backend';
|
||||
import fs from 'node:fs';
|
||||
import type { Canvas } from 'skia-canvas';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { renderField } from '../../universal/field-renderer/render-field';
|
||||
import { getPageSize } from './get-page-size';
|
||||
|
||||
// const font = await pdf.embedFont(
|
||||
// isSignatureField ? fontCaveat : fontNoto,
|
||||
// isSignatureField ? { features: { calt: false } } : undefined,
|
||||
// );
|
||||
// const minFontSize = isSignatureField ? MIN_HANDWRITING_FONT_SIZE : MIN_STANDARD_FONT_SIZE;
|
||||
// const maxFontSize = isSignatureField ? DEFAULT_HANDWRITING_FONT_SIZE : DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
export const insertFieldInPDFV2 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
const [fontCaveat, fontNoto] = await Promise.all([
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
||||
]);
|
||||
|
||||
const isSignatureField = isSignatureFieldType(field.type);
|
||||
|
||||
pdf.registerFontkit(fontkit);
|
||||
|
||||
const pages = pdf.getPages();
|
||||
|
||||
const page = pages.at(field.page - 1);
|
||||
|
||||
if (!page) {
|
||||
throw new Error(`Page ${field.page} does not exist`);
|
||||
}
|
||||
|
||||
const pageRotation = page.getRotation();
|
||||
|
||||
let pageRotationInDegrees = match(pageRotation.type)
|
||||
.with(RotationTypes.Degrees, () => pageRotation.angle)
|
||||
.with(RotationTypes.Radians, () => radiansToDegrees(pageRotation.angle))
|
||||
.exhaustive();
|
||||
|
||||
// Round to the closest multiple of 90 degrees.
|
||||
pageRotationInDegrees = Math.round(pageRotationInDegrees / 90) * 90;
|
||||
|
||||
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
|
||||
|
||||
// Todo: Evenloeps - getPageSize this had extra logic? Ask lucas
|
||||
|
||||
console.log({
|
||||
cropBox: page.getCropBox(),
|
||||
mediaBox: page.getMediaBox(),
|
||||
mediaBox2: page.getSize(),
|
||||
});
|
||||
|
||||
const { width: pageWidth, height: pageHeight } = getPageSize(page);
|
||||
|
||||
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
|
||||
// However when we load the PDF in the backend, the rotation is applied.
|
||||
//
|
||||
// To account for this, we swap the width and height for pages that are rotated by 90/270
|
||||
// degrees. This is so we can calculate the virtual position the field was placed if it
|
||||
// was correctly oriented in the frontend.
|
||||
//
|
||||
// Then when we insert the fields, we apply a transformation to the position of the field
|
||||
// so it is rotated correctly.
|
||||
if (isPageRotatedToLandscape) {
|
||||
// [pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
|
||||
console.log({
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
fieldWidth: field.width,
|
||||
fieldHeight: field.height,
|
||||
});
|
||||
|
||||
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
||||
const layer = new Konva.Layer();
|
||||
|
||||
// Will render onto the layer.
|
||||
renderField({
|
||||
field: {
|
||||
renderId: field.id.toString(),
|
||||
...field,
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
},
|
||||
pageLayer: layer,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
mode: 'export',
|
||||
});
|
||||
|
||||
stage.add(layer);
|
||||
const canvas = layer.canvas._canvas as unknown as Canvas;
|
||||
|
||||
const renderedField = await canvas.toBuffer('svg');
|
||||
|
||||
fs.writeFileSync(
|
||||
`rendered-field-${field.envelopeId}--${field.id}.svg`,
|
||||
renderedField.toString('utf-8'),
|
||||
);
|
||||
|
||||
// Embed the SVG into the PDF
|
||||
const svgElement = await pdf.embedSvg(renderedField.toString('utf-8'));
|
||||
|
||||
// Calculate position to cover the whole page
|
||||
// pdf-lib coordinates: (0,0) is bottom-left, y increases upward
|
||||
const svgWidth = pageWidth; // Use full page width
|
||||
const svgHeight = pageHeight; // Use full page height
|
||||
|
||||
const x = 0; // Start from left edge
|
||||
const y = pageHeight; // Start from bottom edge
|
||||
|
||||
// Draw the SVG on the page
|
||||
page.drawSvg(svgElement, {
|
||||
x: x,
|
||||
y: y,
|
||||
width: svgWidth,
|
||||
height: svgHeight,
|
||||
});
|
||||
|
||||
return pdf;
|
||||
};
|
||||
@ -1,4 +1,10 @@
|
||||
import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib';
|
||||
import {
|
||||
PDFCheckBox,
|
||||
PDFDocument,
|
||||
PDFDropdown,
|
||||
PDFRadioGroup,
|
||||
PDFTextField,
|
||||
} from '@cantoo/pdf-lib';
|
||||
|
||||
export type InsertFormValuesInPdfOptions = {
|
||||
pdf: Buffer;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||
|
||||
export async function insertImageInPDF(
|
||||
pdfAsBase64: string,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
|
||||
import { CAVEAT_FONT_PATH } from '../../constants/pdf';
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
||||
import type { PDFDocument } from '@cantoo/pdf-lib';
|
||||
import { RotationTypes, degrees, radiansToDegrees, rgb } from '@cantoo/pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import type { PDFDocument } from 'pdf-lib';
|
||||
import { RotationTypes, degrees, radiansToDegrees, rgb } from 'pdf-lib';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||
|
||||
import { flattenAnnotations } from './flatten-annotations';
|
||||
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { PDFDocument } from 'pdf-lib';
|
||||
import { PDFSignature, rectangle } from 'pdf-lib';
|
||||
import type { PDFDocument } from '@cantoo/pdf-lib';
|
||||
import { PDFSignature, rectangle } from '@cantoo/pdf-lib';
|
||||
|
||||
export const normalizeSignatureAppearances = (document: PDFDocument) => {
|
||||
const form = document.getForm();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Template, TemplateDirectLink } from '@prisma/client';
|
||||
import { type TeamProfile, TemplateType } from '@prisma/client';
|
||||
import type { Envelope, TemplateDirectLink } from '@prisma/client';
|
||||
import { EnvelopeType, type TeamProfile, TemplateType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -9,11 +9,8 @@ export type GetPublicProfileByUrlOptions = {
|
||||
profileUrl: string;
|
||||
};
|
||||
|
||||
type PublicDirectLinkTemplate = Template & {
|
||||
type: 'PUBLIC';
|
||||
directLink: TemplateDirectLink & {
|
||||
enabled: true;
|
||||
};
|
||||
type PublicDirectLinkTemplate = Pick<Envelope, 'id' | 'publicTitle' | 'publicDescription'> & {
|
||||
directLink: TemplateDirectLink;
|
||||
};
|
||||
|
||||
type GetPublicProfileByUrlResponse = {
|
||||
@ -43,12 +40,13 @@ export const getPublicProfileByUrl = async ({
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
templates: {
|
||||
envelopes: {
|
||||
where: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
templateType: TemplateType.PUBLIC,
|
||||
directLink: {
|
||||
enabled: true,
|
||||
},
|
||||
type: TemplateType.PUBLIC,
|
||||
},
|
||||
include: {
|
||||
directLink: true,
|
||||
@ -68,13 +66,28 @@ export const getPublicProfileByUrl = async ({
|
||||
type: 'Premium',
|
||||
since: team.createdAt,
|
||||
},
|
||||
profile: team.profile,
|
||||
profile: {
|
||||
teamId: team.profile.teamId,
|
||||
id: team.profile.id,
|
||||
enabled: team.profile.enabled,
|
||||
bio: team.profile.bio,
|
||||
},
|
||||
url: profileUrl,
|
||||
avatarImageId: team.avatarImageId,
|
||||
name: team.name || '',
|
||||
templates: team.templates.filter(
|
||||
(template): template is PublicDirectLinkTemplate =>
|
||||
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
|
||||
),
|
||||
templates: team.envelopes.map((template) => {
|
||||
const directLink = template.directLink;
|
||||
|
||||
if (!directLink || !directLink.enabled || template.templateType !== TemplateType.PUBLIC) {
|
||||
throw new Error('Not possible');
|
||||
}
|
||||
|
||||
return {
|
||||
id: template.id,
|
||||
publicTitle: template.publicTitle,
|
||||
publicDescription: template.publicDescription,
|
||||
directLink,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecipientRole } 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';
|
||||
@ -11,12 +11,14 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { getDocumentWhereInput } from '../document/get-document-by-id';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateDocumentRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipients: {
|
||||
email: string;
|
||||
name: string;
|
||||
@ -31,18 +33,19 @@ export interface CreateDocumentRecipientsOptions {
|
||||
export const createDocumentRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
id,
|
||||
recipients: recipientsToCreate,
|
||||
requestMetadata,
|
||||
}: CreateDocumentRecipientsOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
@ -57,13 +60,13 @@ export const createDocumentRecipients = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
@ -74,7 +77,7 @@ export const createDocumentRecipients = async ({
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
@ -95,7 +98,7 @@ export const createDocumentRecipients = async ({
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
documentId,
|
||||
envelopeId: envelope.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
@ -112,7 +115,7 @@ export const createDocumentRecipients = async ({
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
documentId: documentId,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: createdRecipient.email,
|
||||
@ -131,6 +134,8 @@ export const createDocumentRecipients = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
recipients: createdRecipients,
|
||||
recipients: createdRecipients.map((recipient) =>
|
||||
mapRecipientToLegacyRecipient(recipient, envelope),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
@ -8,7 +8,8 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateTemplateRecipientsOptions {
|
||||
userId: number;
|
||||
@ -30,11 +31,18 @@ export const createTemplateRecipients = async ({
|
||||
templateId,
|
||||
recipients: recipientsToCreate,
|
||||
}: CreateTemplateRecipientsOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const template = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
@ -81,7 +89,7 @@ export const createTemplateRecipients = async ({
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
templateId,
|
||||
envelopeId: template.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
@ -100,6 +108,8 @@ export const createTemplateRecipients = async ({
|
||||
});
|
||||
|
||||
return {
|
||||
recipients: createdRecipients,
|
||||
recipients: createdRecipients.map((recipient) =>
|
||||
mapRecipientToLegacyRecipient(recipient, template),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { SendStatus } from '@prisma/client';
|
||||
import { EnvelopeType, SendStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||
@ -30,9 +30,10 @@ export const deleteDocumentRecipient = async ({
|
||||
teamId,
|
||||
recipientId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentRecipientOptions): Promise<void> => {
|
||||
const document = await prisma.document.findFirst({
|
||||
}: DeleteDocumentRecipientOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
@ -62,13 +63,13 @@ export const deleteDocumentRecipient = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
});
|
||||
@ -80,7 +81,7 @@ export const deleteDocumentRecipient = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToDelete = document.recipients[0];
|
||||
const recipientToDelete = envelope.recipients[0];
|
||||
|
||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
@ -88,17 +89,11 @@ export const deleteDocumentRecipient = async ({
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
documentId: document.id,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipientToDelete.email,
|
||||
@ -108,10 +103,16 @@ export const deleteDocumentRecipient = async ({
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
envelope.documentMeta,
|
||||
).recipientRemoved;
|
||||
|
||||
// Send email to deleted recipient.
|
||||
@ -119,8 +120,8 @@ export const deleteDocumentRecipient = async ({
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
documentName: document.title,
|
||||
inviterName: document.team?.name || user.name || undefined,
|
||||
documentName: envelope.title,
|
||||
inviterName: envelope.team?.name || user.name || undefined,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
@ -128,9 +129,9 @@ export const deleteDocumentRecipient = async ({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: document.teamId,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: document.documentMeta,
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
@ -152,4 +153,6 @@ export const deleteDocumentRecipient = async ({
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
return deletedRecipient;
|
||||
};
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
import { SendStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteRecipientOptions = {
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteRecipient = async ({
|
||||
documentId,
|
||||
recipientId,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: DeleteRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
document: {
|
||||
id: documentId,
|
||||
userId,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (recipient.sendStatus !== SendStatus.NOT_SENT) {
|
||||
throw new Error('Can not delete a recipient that has already been sent a document');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({ teamId, userId }),
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
const deleted = await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'RECIPIENT_DELETED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return deleted;
|
||||
});
|
||||
|
||||
return deletedRecipient;
|
||||
};
|
||||
@ -1,7 +1,10 @@
|
||||
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;
|
||||
@ -14,31 +17,31 @@ export const deleteTemplateRecipient = async ({
|
||||
teamId,
|
||||
recipientId,
|
||||
}: DeleteTemplateRecipientOptions): Promise<void> => {
|
||||
const template = await prisma.template.findFirst({
|
||||
const recipientToDelete = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
id: recipientId,
|
||||
envelope: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
if (!recipientToDelete) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToDelete = template.recipients[0];
|
||||
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, {
|
||||
@ -49,6 +52,7 @@ export const deleteTemplateRecipient = async ({
|
||||
await prisma.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user