Compare commits

..

1 Commits

Author SHA1 Message Date
b685190b1a wip: what if user ids were strings instead of numbers 2025-01-03 16:23:35 +11:00
218 changed files with 10662 additions and 8331 deletions

View File

@ -16,7 +16,7 @@
"@documenso/tailwind-config": "*",
"@documenso/trpc": "*",
"@documenso/ui": "*",
"next": "14.2.23",
"next": "14.2.6",
"next-plausible": "^3.12.0",
"nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4",
@ -27,6 +27,6 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "5.2.2"
"typescript": "^5"
}
}

View File

@ -13,11 +13,11 @@
"dependencies": {
"@documenso/prisma": "*",
"luxon": "^3.5.0",
"next": "14.2.23"
"next": "14.2.6"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"typescript": "5.2.2"
"@types/node": "20.16.5",
"@types/react": "18.3.5",
"typescript": "5.5.4"
}
}
}

View File

@ -3,4 +3,4 @@
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.5",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -33,12 +33,12 @@
"framer-motion": "^10.12.8",
"input-otp": "^1.2.4",
"lucide-react": "^0.279.0",
"luxon": "^3.5.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "14.2.23",
"next": "14.2.6",
"next-auth": "4.24.5",
"next-axiom": "^1.5.1",
"next-plausible": "^3.12.0",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
"papaparse": "^5.4.1",
"perfect-freehand": "^1.2.0",
@ -68,11 +68,11 @@
"@simplewebauthn/types": "^9.0.1",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "^20",
"@types/node": "20.1.0",
"@types/papaparse": "^5.3.14",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/ua-parser-js": "^0.7.39",
"typescript": "5.2.2"
}
}
}

View File

@ -25,7 +25,7 @@ export type DocumentPageViewButtonProps = {
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
export const DocumentPageViewButton = ({ document, team }: DocumentPageViewButtonProps) => {
const { data: session } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
@ -48,6 +48,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
documentId: document.id,
teamId: team?.id,
});
const documentData = documentWithData?.documentData;

View File

@ -76,6 +76,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
try {
const documentWithData = await trpcClient.document.getDocumentById.query({
documentId: document.id,
teamId: team?.id,
});
const documentData = documentWithData?.documentData;

View File

@ -10,7 +10,7 @@ import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import type { Document, Recipient, User } from '@documenso/prisma/client';
export type DocumentPageViewInformationProps = {
userId: number;
userId: string;
document: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];

View File

@ -16,7 +16,7 @@ import { cn } from '@documenso/ui/lib/utils';
export type DocumentPageViewRecentActivityProps = {
documentId: number;
userId: number;
userId: string;
};
export const DocumentPageViewRecentActivity = ({

View File

@ -125,7 +125,6 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
getFieldsForDocument({
documentId,
userId: user.id,
teamId: team?.id,
}),
]);

View File

@ -64,6 +64,7 @@ export const EditDocumentForm = ({
trpc.document.getDocumentWithDetailsById.useQuery(
{
documentId: initialDocument.id,
teamId: team?.id,
},
{
initialData: initialDocument,
@ -73,12 +74,13 @@ export const EditDocumentForm = ({
const { Recipient: recipients, Field: fields } = document;
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
const { mutateAsync: setSettingsForDocument } = trpc.document.setSettingsForDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
);
@ -92,6 +94,7 @@ export const EditDocumentForm = ({
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
);
@ -104,18 +107,38 @@ export const EditDocumentForm = ({
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), Field: newFields }),
);
},
});
const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
const { mutateAsync: updateTypedSignature } =
trpc.document.updateTypedSignatureSettings.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({
...(oldData || initialDocument),
...newData,
id: Number(newData.id),
}),
);
},
});
const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: ({ recipients: newRecipients }) => {
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), Recipient: newRecipients }),
);
@ -128,6 +151,7 @@ export const EditDocumentForm = ({
utils.document.getDocumentWithDetailsById.setData(
{
documentId: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
);
@ -181,8 +205,9 @@ export const EditDocumentForm = ({
try {
const { timezone, dateFormat, redirectUrl, language } = data.meta;
await updateDocument({
await setSettingsForDocument({
documentId: document.id,
teamId: team?.id,
data: {
title: data.title,
externalId: data.externalId || null,
@ -221,9 +246,10 @@ export const EditDocumentForm = ({
signingOrder: data.signingOrder,
}),
setRecipients({
addSigners({
documentId: document.id,
recipients: data.signers.map((signer) => ({
teamId: team?.id,
signers: data.signers.map((signer) => ({
...signer,
// Explicitly set to null to indicate we want to remove auth if required.
actionAuth: signer.actionAuth || null,
@ -253,12 +279,9 @@ export const EditDocumentForm = ({
fields: data.fields,
});
await updateDocument({
await updateTypedSignature({
documentId: document.id,
meta: {
typedSignatureEnabled: data.typedSignatureEnabled,
},
typedSignatureEnabled: data.typedSignatureEnabled,
});
// Clear all field data from localStorage
@ -290,6 +313,7 @@ export const EditDocumentForm = ({
try {
await sendDocument({
documentId: document.id,
teamId: team?.id,
meta: {
subject,
message,

View File

@ -15,7 +15,11 @@ export type DownloadAuditLogButtonProps = {
documentId: number;
};
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
export const DownloadAuditLogButton = ({
className,
teamId,
documentId,
}: DownloadAuditLogButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
@ -24,7 +28,7 @@ export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditL
const onDownloadAuditLogsClick = async () => {
try {
const { url } = await downloadAuditLogs({ documentId });
const { url } = await downloadAuditLogs({ teamId, documentId });
const iframe = Object.assign(document.createElement('iframe'), {
src: url,

View File

@ -31,7 +31,7 @@ export const DownloadCertificateButton = ({
const onDownloadCertificatesClick = async () => {
try {
const { url } = await downloadCertificate({ documentId });
const { url } = await downloadCertificate({ documentId, teamId });
const iframe = Object.assign(document.createElement('iframe'), {
src: url,

View File

@ -91,7 +91,7 @@ export const ResendDocumentActionItem = ({
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
try {
await resendDocument({ documentId: document.id, recipients });
await resendDocument({ documentId: document.id, recipients, teamId: team?.id });
toast({
title: _(msg`Document re-sent`),

View File

@ -55,6 +55,7 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
documentId: row.id,
teamId: team?.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({

View File

@ -86,6 +86,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
documentId: row.id,
teamId: team?.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({

View File

@ -38,6 +38,7 @@ export const DeleteDocumentDialog = ({
onOpenChange,
status,
documentTitle,
teamId,
canManageDocument,
}: DeleteDocumentDialogProps) => {
const router = useRouter();
@ -75,7 +76,7 @@ export const DeleteDocumentDialog = ({
const onDelete = async () => {
try {
await deleteDocument({ documentId: id });
await deleteDocument({ documentId: id, teamId });
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -37,6 +37,7 @@ export const DuplicateDocumentDialog = ({
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
documentId: id,
teamId: team?.id,
});
const documentData = document?.documentData
@ -65,7 +66,7 @@ export const DuplicateDocumentDialog = ({
const onDuplicate = async () => {
try {
await duplicateDocument({ documentId: id });
await duplicateDocument({ documentId: id, teamId: team?.id });
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -76,6 +76,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
const { id } = await createDocument({
title: file.name,
documentDataId,
teamId: team?.id,
timezone: userTimezone,
});

View File

@ -61,6 +61,7 @@ export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePage
const { data } = trpc.template.findTemplates.useQuery({
perPage: 100,
teamId: team?.id,
});
const { mutateAsync: updateUserProfile, isLoading: isUpdatingUserProfile } =

View File

@ -23,12 +23,15 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
import { useOptionalCurrentTeam } from '~/providers/team';
type DirectTemplate = FindTemplateRow & {
directLink: Pick<TemplateDirectLink, 'token' | 'enabled'>;
};
export const PublicTemplatesDataTable = () => {
const team = useOptionalCurrentTeam();
const { _ } = useLingui();
const { toast } = useToast();
@ -40,7 +43,9 @@ export const PublicTemplatesDataTable = () => {
} | null>(null);
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
{},
{
teamId: team?.id,
},
{
keepPreviousData: true,
},

View File

@ -62,6 +62,7 @@ export const EditTemplateForm = ({
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
{
templateId: initialTemplate.id,
teamId: initialTemplate.teamId || undefined,
},
{
initialData: initialTemplate,
@ -103,6 +104,19 @@ export const EditTemplateForm = ({
},
});
const { mutateAsync: setSigningOrderForTemplate } =
trpc.template.setSigningOrderForTemplate.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateById.setData(
{
templateId: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
},
});
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
@ -115,7 +129,7 @@ export const EditTemplateForm = ({
},
});
const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateById.setData(
@ -127,14 +141,31 @@ export const EditTemplateForm = ({
},
});
const { mutateAsync: updateTypedSignature } =
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateById.setData(
{
templateId: initialTemplate.id,
},
(oldData) => ({
...(oldData || initialTemplate),
...newData,
id: Number(newData.id),
}),
);
},
});
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
try {
await updateTemplateSettings({
templateId: template.id,
teamId: team?.id,
data: {
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null,
},
@ -164,16 +195,16 @@ export const EditTemplateForm = ({
) => {
try {
await Promise.all([
updateTemplateSettings({
setSigningOrderForTemplate({
templateId: template.id,
meta: {
signingOrder: data.signingOrder,
},
teamId: team?.id,
signingOrder: data.signingOrder,
}),
setRecipients({
addTemplateSigners({
templateId: template.id,
recipients: data.signers,
teamId: team?.id,
signers: data.signers,
}),
]);
@ -197,11 +228,10 @@ export const EditTemplateForm = ({
fields: data.fields,
});
await updateTemplateSettings({
await updateTypedSignature({
templateId: template.id,
meta: {
typedSignatureEnabled: data.typedSignatureEnabled,
},
teamId: team?.id,
typedSignatureEnabled: data.typedSignatureEnabled,
});
// Clear all field data from localStorage
@ -266,7 +296,6 @@ export const EditTemplateForm = ({
<AddTemplateSettingsFormPartial
key={recipients.length}
template={template}
currentTeamMemberRole={team?.currentTeamMember?.role}
documentFlow={documentFlow.settings}
recipients={recipients}
fields={fields}

View File

@ -73,6 +73,7 @@ export const TemplatePageViewDocumentsTable = ({
trpc.document.findDocuments.useQuery(
{
templateId,
teamId: team?.id,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
query: parsedSearchParams.query,

View File

@ -10,7 +10,7 @@ import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import type { Template, User } from '@documenso/prisma/client';
export type TemplatePageViewInformationProps = {
userId: number;
userId: string;
template: Template & {
User: Pick<User, 'id' | 'name' | 'email'>;
};

View File

@ -20,10 +20,12 @@ export type TemplatePageViewRecentActivityProps = {
export const TemplatePageViewRecentActivity = ({
templateId,
teamId,
documentRootPath,
}: TemplatePageViewRecentActivityProps) => {
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
templateId,
teamId,
orderByColumn: 'createdAt',
orderByDirection: 'asc',
perPage: 5,

View File

@ -22,7 +22,12 @@ type DeleteTemplateDialogProps = {
onOpenChange: (_open: boolean) => void;
};
export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
export const DeleteTemplateDialog = ({
id,
teamId,
open,
onOpenChange,
}: DeleteTemplateDialogProps) => {
const router = useRouter();
const { _ } = useLingui();
@ -80,7 +85,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
type="button"
variant="destructive"
loading={isLoading}
onClick={async () => deleteTemplate({ templateId: id })}
onClick={async () => deleteTemplate({ templateId: id, teamId })}
>
<Trans>Delete</Trans>
</Button>

View File

@ -24,6 +24,7 @@ type DuplicateTemplateDialogProps = {
export const DuplicateTemplateDialog = ({
id,
teamId,
open,
onOpenChange,
}: DuplicateTemplateDialogProps) => {
@ -83,6 +84,7 @@ export const DuplicateTemplateDialog = ({
onClick={async () =>
duplicateTemplate({
templateId: id,
teamId,
})
}
>

View File

@ -31,7 +31,7 @@ type NewTemplateDialogProps = {
templateRootPath: string;
};
export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => {
const router = useRouter();
const { data: session } = useSession();
@ -58,6 +58,7 @@ export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps)
});
const { id } = await createTemplate({
teamId,
title: file.name,
templateDocumentDataId,
});

View File

@ -174,6 +174,7 @@ export const TemplateDirectLinkDialog = ({
await createTemplateDirectLink({
templateId: template.id,
teamId: team?.id,
directRecipientId: recipientId,
});
};
@ -344,6 +345,7 @@ export const TemplateDirectLinkDialog = ({
onClick={async () =>
createTemplateDirectLink({
templateId: template.id,
teamId: team?.id,
})
}
>

View File

@ -7,17 +7,15 @@ import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
import { InfoIcon, Plus } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import * as z from 'zod';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import {
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
} from '@documenso/lib/constants/template';
import { AppError } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import type { Recipient } from '@documenso/prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
@ -47,14 +45,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
import type { Toast } from '@documenso/ui/primitives/use-toast';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
const ZAddRecipientsForNewDocumentSchema = z
.object({
distributeDocument: z.boolean(),
useCustomDocument: z.boolean().default(false),
customDocumentData: z
.any()
.refine((data) => data instanceof File || data === undefined)
.optional(),
recipients: z.array(
z.object({
id: z.number(),
@ -118,12 +113,12 @@ export function UseTemplateDialog({
const [open, setOpen] = useState(false);
const team = useOptionalCurrentTeam();
const form = useForm<TAddRecipientsForNewDocumentSchema>({
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
defaultValues: {
distributeDocument: false,
useCustomDocument: false,
customDocumentData: undefined,
recipients: recipients
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
.map((recipient) => {
@ -150,18 +145,11 @@ export function UseTemplateDialog({
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
try {
let customDocumentDataId: string | undefined = undefined;
if (data.useCustomDocument && data.customDocumentData) {
const customDocumentData = await putPdfFile(data.customDocumentData);
customDocumentDataId = customDocumentData.id;
}
const { id } = await createDocumentFromTemplate({
templateId,
teamId: team?.id,
recipients: data.recipients,
distributeDocument: data.distributeDocument,
customDocumentDataId,
});
toast({
@ -312,245 +300,89 @@ export function UseTemplateDialog({
/>
</div>
))}
{recipients.length > 0 && (
<div className="mt-4 flex flex-row items-center">
<FormField
control={form.control}
name="distributeDocument"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center">
<Checkbox
id="distributeDocument"
className="h-5 w-5"
checked={field.value}
onCheckedChange={field.onChange}
/>
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="distributeDocument"
>
<Trans>Send document</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
<Trans>
The document will be immediately sent to recipients if this
is checked.
</Trans>
</p>
<p>
<Trans>
Otherwise, the document will be created as a draft.
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
)}
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="distributeDocument"
>
<Trans>Create as pending</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
<Trans>
Create the document as pending and ready to sign.
</Trans>
</p>
<p>
<Trans>We won't send anything to notify recipients.</Trans>
</p>
<p className="mt-2">
<Trans>
We will generate signing links for you, which you can send
to the recipients through your method of choice.
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
)}
</div>
</FormItem>
)}
/>
</div>
)}
<FormField
control={form.control}
name="useCustomDocument"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center">
<Checkbox
id="useCustomDocument"
className="h-5 w-5"
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
if (!checked) {
form.setValue('customDocumentData', undefined);
}
}}
/>
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="useCustomDocument"
>
<Trans>Upload custom document</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
<Trans>
Upload a custom document to use instead of the template's default
document
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
</div>
</FormItem>
)}
/>
{form.watch('useCustomDocument') && (
<div className="my-4">
<FormField
control={form.control}
name="customDocumentData"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="w-full space-y-4">
<label
className={cn(
'text-muted-foreground hover:border-muted-foreground/50 group relative flex min-h-[150px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 transition-colors',
{
'border-destructive hover:border-destructive':
form.formState.errors.customDocumentData,
},
)}
>
<div className="text-center">
{!field.value && (
<>
<Upload className="text-muted-foreground/50 mx-auto h-10 w-10" />
<div className="mt-4 flex text-sm leading-6">
<span className="text-muted-foreground relative">
<Trans>
<span className="text-primary font-semibold">
Click to upload
</span>{' '}
or drag and drop
</Trans>
</span>
</div>
<p className="text-muted-foreground/80 text-xs">
PDF files only
</p>
</>
)}
{field.value && (
<div className="text-muted-foreground space-y-1">
<p className="text-sm font-medium">{field.value.name}</p>
<p className="text-muted-foreground/60 text-xs">
{(field.value.size / (1024 * 1024)).toFixed(2)} MB
</p>
</div>
)}
</div>
<input
type="file"
className="absolute h-full w-full opacity-0"
accept=".pdf,application/pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) {
field.onChange(undefined);
return;
}
if (file.type !== 'application/pdf') {
form.setError('customDocumentData', {
type: 'manual',
message: _(msg`Please select a PDF file`),
});
return;
}
if (file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024) {
form.setError('customDocumentData', {
type: 'manual',
message: _(
msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`,
),
});
return;
}
field.onChange(file);
}}
/>
{field.value && (
<div className="absolute right-2 top-2">
<Button
type="button"
variant="destructive"
className="h-6 w-6 p-0"
onClick={(e) => {
e.preventDefault();
field.onChange(undefined);
}}
>
<X className="h-4 w-4" />
<div className="sr-only">
<Trans>Clear file</Trans>
</div>
</Button>
</div>
)}
</label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div>
<DialogFooter className="mt-4">
{recipients.length > 0 && (
<div className="mt-4 flex flex-row items-center">
<FormField
control={form.control}
name="distributeDocument"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center">
<Checkbox
id="distributeDocument"
className="h-5 w-5"
checked={field.value}
onCheckedChange={field.onChange}
/>
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="distributeDocument"
>
<Trans>Send document</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
<Trans>
The document will be immediately sent to recipients if this is
checked.
</Trans>
</p>
<p>
<Trans>
Otherwise, the document will be created as a draft.
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
)}
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="distributeDocument"
>
<Trans>Create as pending</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
<Trans>Create the document as pending and ready to sign.</Trans>
</p>
<p>
<Trans>We won't send anything to notify recipients.</Trans>
</p>
<p className="mt-2">
<Trans>
We will generate signing links for you, which you can send to
the recipients through your method of choice.
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
)}
</div>
</FormItem>
)}
/>
</div>
)}
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
<Trans>Close</Trans>

View File

@ -66,7 +66,6 @@ export default async function AuditLog({ searchParams }: AuditLogProps) {
const { data: auditLogs } = await findDocumentAuditLogs({
documentId: documentId,
userId: document.userId,
teamId: document.teamId || undefined,
perPage: 100_000,
});

View File

@ -8,7 +8,6 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { TrpcProvider } from '@documenso/trpc/react';
import { Header } from '~/components/(dashboard)/layout/header';
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
@ -48,10 +47,6 @@ export default async function AuthenticatedTeamsLayout({
const team = getTeamPromise.value;
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
const trpcHeaders = {
'x-team-Id': team.id.toString(),
};
return (
<NextAuthProvider session={session}>
<LimitsProvider teamId={team.id}>
@ -66,9 +61,7 @@ export default async function AuthenticatedTeamsLayout({
<Header user={user} teams={teams} />
<TeamProvider team={team}>
<TrpcProvider headers={trpcHeaders}>
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
</TrpcProvider>
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
</TeamProvider>
<RefreshOnFocus />

View File

@ -45,7 +45,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type TransferTeamDialogProps = {
teamId: number;
teamName: string;
ownerUserId: number;
ownerUserId: string;
trigger?: React.ReactNode;
};
@ -98,7 +98,7 @@ export const TransferTeamDialog = ({
try {
await requestTeamOwnershipTransfer({
teamId,
newOwnerUserId: Number.parseInt(newOwnerUserId),
newOwnerUserId,
clearPaymentMethods,
});

View File

@ -34,7 +34,7 @@ import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
export type TeamMembersDataTableProps = {
currentUserTeamRole: TeamMemberRole;
teamOwnerUserId: number;
teamOwneruserId: string;
teamId: number;
teamName: string;
};

View File

@ -20,7 +20,7 @@ export type TeamsMemberPageDataTableProps = {
currentUserTeamRole: TeamMemberRole;
teamId: number;
teamName: string;
teamOwnerUserId: number;
teamOwneruserId: string;
};
export const TeamsMemberPageDataTable = ({

View File

@ -24,7 +24,7 @@ import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
export type DocumentHistorySheetProps = {
documentId: number;
userId: number;
userId: string;
isMenuOpen?: boolean;
onMenuOpenChange?: (_value: boolean) => void;
children?: React.ReactNode;

View File

@ -123,6 +123,7 @@ export const ManagePublicTemplateDialog = ({
try {
await updateTemplateSettings({
templateId,
teamId: team?.id,
data: {
type: TemplateType.PRIVATE,
},
@ -157,6 +158,7 @@ export const ManagePublicTemplateDialog = ({
try {
await updateTemplateSettings({
templateId: selectedTemplateId,
teamId: team?.id,
data: {
type: TemplateType.PUBLIC,
publicTitle,

View File

@ -13,7 +13,7 @@ const logger = buildLogger();
export default createOpenApiNextHandler<typeof appRouter>({
router: appRouter,
createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) =>
createTrpcContext({ req, res, requestSource: 'apiV2' }),
createTrpcContext({ req, res }),
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
// Always log the error for now.
console.error(error.message);

View File

@ -17,7 +17,7 @@ const logger = buildLogger();
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: async ({ req, res }) => createTrpcContext({ req, res, requestSource: 'app' }),
createContext: async ({ req, res }) => createTrpcContext({ req, res }),
onError(opts) {
const { error, path } = opts;

11805
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.5",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",
@ -69,14 +69,17 @@
"inngest-cli": "^0.29.1",
"luxon": "^3.5.0",
"mupdf": "^1.0.0",
"next": "14.2.23",
"next-runtime-env": "^3.2.0",
"@prisma/client": "^6.1.0",
"prisma": "^6.1.0",
"react": "^18",
"zod": "3.24.1"
},
"overrides": {
"zod": "3.24.1",
"next": "14.2.23"
"next": "14.2.6",
"@prisma/client": "^6.1.0",
"prisma": "^6.1.0",
"zod": "3.24.1"
},
"trigger.dev": {
"endpointId": "documenso-app"

View File

@ -21,7 +21,7 @@
"@ts-rest/next": "^3.30.5",
"@ts-rest/open-api": "^3.33.0",
"@types/swagger-ui-react": "^4.18.3",
"luxon": "^3.5.0",
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"swagger-ui-react": "^5.11.0",
"ts-pattern": "^5.0.5",

View File

@ -1,6 +1,5 @@
import { createNextRoute } from '@ts-rest/next';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@ -16,7 +15,8 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
@ -25,7 +25,7 @@ import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-for
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
import { getRecipientByIdV1Api } from '@documenso/lib/server-only/recipient/get-recipient-by-id-v1-api';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
@ -36,10 +36,10 @@ import { deleteTemplate } from '@documenso/lib/server-only/template/delete-templ
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZFieldMetaSchema,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
@ -53,7 +53,6 @@ import {
} from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import {
DocumentDataType,
DocumentStatus,
@ -63,7 +62,6 @@ import {
import { ApiContractV1 } from './contract';
import { authenticatedMiddleware } from './middleware/authenticated';
import { ZTemplateWithDataSchema } from './schema';
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
getDocuments: authenticatedMiddleware(async (args, user, team) => {
@ -98,14 +96,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
const recipients = await getRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
userId: user.id,
});
const fields = await getFieldsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
const parsedMetaFields = fields.map((field) => {
@ -210,7 +207,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
try {
@ -233,7 +230,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
id: document.id,
userId: user.id,
teamId: team?.id,
requestMetadata: metadata,
});
return {
@ -250,7 +246,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createDocument: authenticatedMiddleware(async (args, user, team) => {
const { body } = args;
try {
@ -318,13 +314,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
teamId: team?.id,
formValues: body.formValues,
documentDataId: documentData.id,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
subject: body.meta.subject,
message: body.meta.message,
timezone,
@ -335,7 +330,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
typedSignatureEnabled: body.meta.typedSignatureEnabled,
distributionMethod: body.meta.distributionMethod,
emailSettings: body.meta.emailSettings,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
if (body.authOptions) {
@ -346,16 +341,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
data: {
...body.authOptions,
},
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
const { recipients } = await setDocumentRecipients({
const { recipients } = await setRecipientsForDocument({
userId: user.id,
teamId: team?.id,
documentId: document.id,
recipients: body.recipients,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
return {
@ -419,11 +414,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
teamId: team?.id,
});
const parsed = ZTemplateWithDataSchema.parse(template);
return {
status: 200,
body: parsed,
body: template,
};
} catch (err) {
return AppError.toRestAPIError(err);
@ -442,12 +435,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
teamId: team?.id,
});
const parsed = z.array(ZTemplateWithDataSchema).parse(templates);
return {
status: 200,
body: {
templates: parsed,
templates,
totalPages,
},
};
@ -456,7 +447,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
@ -520,9 +511,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
@ -532,7 +522,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
@ -554,7 +544,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
};
}),
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
@ -583,7 +573,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
@ -626,7 +615,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
@ -647,7 +636,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
};
}),
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
sendDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const { sendEmail, sendCompletionEmails } = args.body;
@ -683,13 +672,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
emailSettings: {
...emailSettings,
documentCompleted: sendCompletionEmails,
ownerDocumentCompleted: sendCompletionEmails,
},
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
}
@ -698,7 +686,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
sendEmail,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
return {
@ -722,7 +710,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
resendDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const { recipients } = args.body;
@ -732,7 +720,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
documentId: Number(documentId),
recipients,
teamId: team?.id,
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
return {
@ -751,7 +739,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createRecipient: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
@ -797,7 +785,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
try {
const { recipients: newRecipients } = await setDocumentRecipients({
const { recipients: newRecipients } = await setRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
@ -815,7 +803,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
actionAuth: authOptions?.actionAuth ?? null,
},
],
requestMetadata: metadata,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
@ -1580,39 +1568,3 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
};
}),
});
const updateDocument = async ({
documentId,
userId,
teamId,
data,
}: {
documentId: number;
data: Prisma.DocumentUpdateInput;
userId: number;
teamId?: number;
}) => {
return await prisma.document.update({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
data: {
...data,
},
});
};

View File

@ -2,8 +2,6 @@ import type { NextApiRequest } from 'next';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Team, User } from '@documenso/prisma/client';
export const authenticatedMiddleware = <
@ -15,12 +13,7 @@ export const authenticatedMiddleware = <
body: unknown;
},
>(
handler: (
args: T,
user: User,
team: Team | null | undefined,
options: { metadata: ApiRequestMetadata },
) => Promise<R>,
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
) => {
return async (args: T) => {
try {
@ -43,18 +36,7 @@ export const authenticatedMiddleware = <
});
}
const metadata: ApiRequestMetadata = {
requestMetadata: extractNextApiRequestMetadata(args.req),
source: 'apiV1',
auth: 'api',
auditUser: {
id: apiToken.team ? null : apiToken.user.id,
email: apiToken.team ? null : apiToken.user.email,
name: apiToken.team?.name ?? apiToken.user.name,
},
};
return await handler(args, apiToken.user, apiToken.team, { metadata });
return await handler(args, apiToken.user, apiToken.team);
} catch (err) {
console.log({ err: err });

View File

@ -61,7 +61,6 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
fields: z.lazy(() =>
ZFieldSchema.pick({
id: true,
documentId: true,
recipientId: true,
type: true,
page: true,
@ -69,8 +68,6 @@ export const ZSuccessfulGetDocumentResponseSchema = ZSuccessfulDocumentResponseS
positionY: true,
width: true,
height: true,
customText: true,
fieldMeta: true,
})
.extend({
fieldMeta: ZFieldMetaSchema.nullish(),
@ -527,7 +524,6 @@ export const ZFieldSchema = z.object({
height: z.unknown(),
customText: z.string(),
inserted: z.boolean(),
fieldMeta: ZFieldMetaSchema.nullish().openapi({}),
});
export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
@ -545,8 +541,6 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
}),
Field: ZFieldSchema.pick({
id: true,
documentId: true,
templateId: true,
recipientId: true,
type: true,
page: true,
@ -554,8 +548,6 @@ export const ZTemplateWithDataSchema = ZTemplateSchema.extend({
positionY: true,
width: true,
height: true,
customText: true,
fieldMeta: true,
}).array(),
Recipient: ZRecipientSchema.pick({
id: true,

View File

@ -1,7 +1,5 @@
import { expect, test } from '@playwright/test';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@ -159,109 +157,3 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
await expect(page.getByLabel('Title')).toHaveValue('New Title');
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
});
test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => {
const { owner, ...team } = await seedTeam({
createTeamMembers: 1,
});
const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Set document visibility.
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
// Save the settings by going to the next step.
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
// Navigate back to the edit page to check that the settings are saved correctly.
await page.goto(`/t/${team.url}/templates/${template.id}/edit`);
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
});
test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
const team = await seedTeam({
createTeamMembers: 2, // Create an additional member to test different roles
});
await prisma.teamMember.update({
where: {
id: team.members[1].id,
},
data: {
role: TeamMemberRole.MANAGER,
},
});
const owner = team.owner;
const managerUser = team.members[1].user;
const memberUser = team.members[2].user;
const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});
// Test as manager
await apiSignin({
page,
email: managerUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Manager should be able to set visibility to managers and above
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await expect(page.getByText('Admins only')).toBeDisabled();
// Save and verify
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
// Test as regular member
await apiSignin({
page,
email: memberUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Regular member should not be able to modify visibility when set to managers and above
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
// Create a new template with 'everyone' visibility
const everyoneTemplate = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
visibility: 'EVERYONE',
},
});
// Navigate to the new template
await page.goto(`/t/${team.url}/templates/${everyoneTemplate.id}/edit`);
// Regular member should be able to see but not modify visibility
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Everyone');
});

View File

@ -1,11 +1,7 @@
import { expect, test } from '@playwright/test';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@ -17,20 +13,6 @@ test.describe.configure({ mode: 'parallel' });
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
// Create a temporary PDF file for testing
function createTempPdfFile() {
const tempDir = os.tmpdir();
const tempFilePath = path.join(tempDir, 'test.pdf');
// Create a simple PDF file with some content
const pdfContent = Buffer.from(
'%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000052 00000 n\n0000000101 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n178\n%%EOF',
);
fs.writeFileSync(tempFilePath, pdfContent);
return tempFilePath;
}
/**
* 1. Create a template with all settings filled out
* 2. Create a document from the template
@ -301,318 +283,3 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
});
/**
* This test verifies that we can create a document from a template using a custom document
* instead of the template's default document.
*/
test('[TEMPLATE]: should create a document from a template with custom document', async ({
page,
}) => {
const user = await seedUser();
const template = await seedBlankTemplate(user);
// Create a temporary PDF file for upload
const testPdfPath = createTempPdfFile();
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
try {
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}/edit`,
});
// Set template title
await page.getByLabel('Title').fill('TEMPLATE_WITH_CUSTOM_DOC');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
// Add a signer
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Save template' }).click();
// Use template with custom document
await page.waitForURL('/templates');
await page.getByRole('button', { name: 'Use Template' }).click();
// Enable custom document upload and upload file
await page.getByLabel('Upload custom document').check();
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
// Wait for upload to complete
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
// Create document with custom document data
await page.getByRole('button', { name: 'Create as draft' }).click();
// Review that the document was created with the custom document data
await page.waitForURL(/documents/);
const documentId = Number(page.url().split('/').pop());
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
documentData: true,
},
});
expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC');
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
expect(document.documentData.data).toEqual(pdfContent);
expect(document.documentData.initialData).toEqual(pdfContent);
} finally {
// Clean up the temporary file
fs.unlinkSync(testPdfPath);
}
});
/**
* This test verifies that we can create a team document from a template using a custom document
* instead of the template's default document.
*/
test('[TEMPLATE]: should create a team document from a template with custom document', async ({
page,
}) => {
const { owner, ...team } = await seedTeam({
createTeamMembers: 2,
});
const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});
// Create a temporary PDF file for upload
const testPdfPath = createTempPdfFile();
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
try {
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Set template title
await page.getByLabel('Title').fill('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
// Add a signer
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Save template' }).click();
// Use template with custom document
await page.waitForURL(`/t/${team.url}/templates`);
await page.getByRole('button', { name: 'Use Template' }).click();
// Enable custom document upload and upload file
await page.getByLabel('Upload custom document').check();
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
// Wait for upload to complete
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
// Create document with custom document data
await page.getByRole('button', { name: 'Create as draft' }).click();
// Review that the document was created with the custom document data
await page.waitForURL(/documents/);
const documentId = Number(page.url().split('/').pop());
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
documentData: true,
},
});
expect(document.teamId).toEqual(team.id);
expect(document.title).toEqual('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
expect(document.documentData.data).toEqual(pdfContent);
expect(document.documentData.initialData).toEqual(pdfContent);
} finally {
// Clean up the temporary file
fs.unlinkSync(testPdfPath);
}
});
/**
* This test verifies that when custom document upload is not enabled,
* the document uses the template's original document data.
*/
test('[TEMPLATE]: should create a document from a template using template document when custom document is not enabled', async ({
page,
}) => {
const user = await seedUser();
const template = await seedBlankTemplate(user);
await apiSignin({
page,
email: user.email,
redirectPath: `/templates/${template.id}/edit`,
});
// Set template title
await page.getByLabel('Title').fill('TEMPLATE_WITH_ORIGINAL_DOC');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
// Add a signer
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Save template' }).click();
// Use template without custom document
await page.waitForURL('/templates');
await page.getByRole('button', { name: 'Use Template' }).click();
// Verify custom document upload is not checked by default
await expect(page.getByLabel('Upload custom document')).not.toBeChecked();
// Create document without custom document data
await page.getByRole('button', { name: 'Create as draft' }).click();
// Review that the document was created with the template's document data
await page.waitForURL(/documents/);
const documentId = Number(page.url().split('/').pop());
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
include: {
documentData: true,
},
});
const templateWithData = await prisma.template.findFirstOrThrow({
where: {
id: template.id,
},
include: {
templateDocumentData: true,
},
});
expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC');
expect(document.documentData.data).toEqual(templateWithData.templateDocumentData.data);
expect(document.documentData.initialData).toEqual(
templateWithData.templateDocumentData.initialData,
);
expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type);
});
test('[TEMPLATE]: should persist document visibility when creating from template', async ({
page,
}) => {
const { owner, ...team } = await seedTeam({
createTeamMembers: 2,
});
const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});
await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
// Set template title and visibility
await page.getByLabel('Title').fill('TEMPLATE_WITH_VISIBILITY');
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
// Add a signer
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Save template' }).click();
// Test creating document as team manager
await prisma.teamMember.update({
where: {
id: team.members[1].id,
},
data: {
role: TeamMemberRole.MANAGER,
},
});
const managerUser = team.members[1].user;
await apiSignin({
page,
email: managerUser.email,
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: 'Use Template' }).click();
await page.getByRole('button', { name: 'Create as draft' }).click();
// Review that the document was created with the correct visibility
await page.waitForURL(/documents/);
const documentId = Number(page.url().split('/').pop());
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
});
expect(document.title).toEqual('TEMPLATE_WITH_VISIBILITY');
expect(document.visibility).toEqual('MANAGER_AND_ABOVE');
expect(document.teamId).toEqual(team.id);
// Test that regular member cannot create document from restricted template
const memberUser = team.members[2].user;
await apiSignin({
page,
email: memberUser.email,
redirectPath: `/t/${team.url}/templates`,
});
// Template should not be visible to regular member
await expect(page.getByRole('button', { name: 'Use Template' })).not.toBeVisible();
});

View File

@ -67,8 +67,6 @@ test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) =>
await page.getByRole('button', { name: 'Enable direct link signing' }).click();
await page.getByRole('button', { name: 'Create one automatically' }).click();
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
await page.waitForTimeout(1000);
await page.getByTestId('btn-dialog-close').click();
// Expect badge to appear.

View File

@ -13,7 +13,7 @@
"author": "",
"devDependencies": {
"@playwright/test": "^1.18.1",
"@types/node": "^20",
"@types/node": "^20.8.2",
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@documenso/web": "*",

View File

@ -1,4 +1,4 @@
This file lists all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
This file list all features currently licensed under the Documenso Enterprise Edition (the "Commercial License”)
Copyright (c) 2023 Documenso, Inc
- The Stripe Billing Module

View File

@ -15,9 +15,9 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
"luxon": "^3.5.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "14.2.23",
"next": "14.2.6",
"next-auth": "4.24.5",
"react": "^18",
"ts-pattern": "^5.0.5",

View File

@ -91,7 +91,7 @@ export const getStripeCustomerIdByUser = async (user: User) => {
return await getStripeCustomerByUser(user).then((session) => session.stripeCustomer.id);
};
const syncStripeCustomerSubscriptions = async (userId: number, stripeCustomerId: string) => {
const syncStripeCustomerSubscriptions = async (userId: string, stripeCustomerId: string) => {
const stripeSubscriptions = await stripe.subscriptions.list({
customer: stripeCustomerId,
});

View File

@ -6,7 +6,7 @@ import type { Subscription } from '@documenso/prisma/client';
import { getEnterprisePlanPriceIds } from '../stripe/get-enterprise-plan-prices';
export type IsUserEnterpriseOptions = {
userId: number;
userId: string;
teamId?: number;
};

View File

@ -126,7 +126,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
return {
id: Number(user.id),
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
@ -140,7 +140,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
profile(profile) {
return {
id: Number(profile.sub),
id: profile.sub,
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
email: profile.email,
emailVerified: profile.email_verified ? new Date().toISOString() : null,
@ -274,7 +274,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
});
return {
id: Number(user.id),
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
@ -308,7 +308,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
const { userId, email } = parsedCredential;
if (typeof userId !== 'number' || typeof email !== 'string') {
if (typeof userId !== 'string' || typeof email !== 'string') {
throw new AppError(AppErrorCode.INVALID_REQUEST);
}
@ -323,7 +323,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
return {
id: Number(user.id),
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
@ -340,7 +340,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
} satisfies JWT;
if (!merged.email || typeof merged.emailVerified !== 'string') {
const userId = Number(merged.id ?? token.sub);
const userId = merged.id ?? token.sub;
const retrieved = await prisma.user.findFirst({
where: {
@ -367,7 +367,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
const user = await prisma.user.update({
where: {
id: Number(merged.id),
id: merged.id,
},
data: {
lastSignedIn: merged.lastSignedIn,
@ -384,7 +384,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
await prisma.user.update({
where: {
id: Number(merged.id),
id: merged.id,
},
data: {
emailVerified: merged.emailVerified,
@ -407,7 +407,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
return {
...session,
user: {
id: Number(token.id),
id: token.id,
name: token.name,
email: token.email,
emailVerified: token.emailVerified ?? null,

View File

@ -42,10 +42,10 @@
"@vvo/tzdb": "^6.117.0",
"inngest": "^3.19.13",
"kysely": "^0.26.3",
"luxon": "^3.5.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"nanoid": "^4.0.2",
"next": "14.2.23",
"next": "14.2.6",
"next-auth": "4.24.5",
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",

View File

@ -9,7 +9,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import { getAuthenticatorOptions } from '../../utils/authenticator';
type CreatePasskeyAuthenticationOptions = {
userId: number;
userId: string;
/**
* The ID of the passkey to request authentication for.

View File

@ -8,7 +8,7 @@ import { PASSKEY_TIMEOUT } from '../../constants/auth';
import { getAuthenticatorOptions } from '../../utils/authenticator';
type CreatePasskeyRegistrationOptions = {
userId: number;
userId: string;
};
export const createPasskeyRegistrationOptions = async ({

View File

@ -10,7 +10,7 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getAuthenticatorOptions } from '../../utils/authenticator';
type CreatePasskeyOptions = {
userId: number;
userId: string;
passkeyName: string;
verificationResponse: RegistrationResponseJSON;
requestMetadata?: RequestMetadata;

View File

@ -4,7 +4,7 @@ import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
export interface DeletePasskeyOptions {
userId: number;
userId: string;
passkeyId: string;
requestMetadata?: RequestMetadata;
}

View File

@ -5,7 +5,7 @@ import { Prisma } from '@documenso/prisma/client';
import type { FindResultResponse } from '../../types/search-params';
export interface FindPasskeysOptions {
userId: number;
userId: string;
query?: string;
page?: number;
perPage?: number;

View File

@ -11,7 +11,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps {
userId: number;
userId: string;
}
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {

View File

@ -11,7 +11,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendForgotPasswordOptions {
userId: number;
userId: string;
}
export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions) => {

View File

@ -8,7 +8,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendResetPasswordOptions {
userId: number;
userId: string;
}
export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) => {

View File

@ -4,7 +4,7 @@ import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
export interface UpdateAuthenticatorsOptions {
userId: number;
userId: string;
passkeyId: string;
name: string;
requestMetadata?: RequestMetadata;

View File

@ -1,7 +1,7 @@
'use server';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import {
createDocumentAuditLogData,
diffDocumentMetaChanges,
@ -13,8 +13,6 @@ import type { SupportedLanguageCodes } from '../../constants/i18n';
import type { TDocumentEmailSettings } from '../../types/document-email';
export type CreateDocumentMetaOptions = {
userId: number;
teamId?: number;
documentId: number;
subject?: string;
message?: string;
@ -27,18 +25,18 @@ export type CreateDocumentMetaOptions = {
distributionMethod?: DocumentDistributionMethod;
typedSignatureEnabled?: boolean;
language?: SupportedLanguageCodes;
requestMetadata: ApiRequestMetadata;
userId: string;
requestMetadata: RequestMetadata;
};
export const upsertDocumentMeta = async ({
userId,
teamId,
subject,
message,
timezone,
dateFormat,
documentId,
password,
userId,
redirectUrl,
signingOrder,
emailSettings,
@ -47,24 +45,34 @@ export const upsertDocumentMeta = async ({
language,
requestMetadata,
}: CreateDocumentMetaOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
email: true,
name: true,
},
});
const { documentMeta: originalDocumentMeta } = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
},
},
}
: {
userId,
teamId: null,
}),
},
},
],
},
include: {
documentMeta: true,
@ -112,7 +120,8 @@ export const upsertDocumentMeta = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
documentId,
metadata: requestMetadata,
user,
requestMetadata,
data: {
changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta),
},

View File

@ -5,7 +5,7 @@ import type { z } from 'zod';
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 type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
@ -21,13 +21,13 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type CreateDocumentOptions = {
title: string;
externalId?: string | null;
userId: number;
userId: string;
teamId?: number;
documentDataId: string;
formValues?: Record<string, string | number | boolean>;
normalizePdf?: boolean;
timezone?: string;
requestMetadata: ApiRequestMetadata;
requestMetadata?: RequestMetadata;
};
export const ZCreateDocumentResponseSchema = DocumentSchema;
@ -162,7 +162,8 @@ export const createDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id,
metadata: requestMetadata,
user,
requestMetadata,
data: {
title,
source: {

View File

@ -20,19 +20,18 @@ import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
export type DeleteDocumentOptions = {
id: number;
userId: number;
userId: string;
teamId?: number;
requestMetadata: ApiRequestMetadata;
requestMetadata?: RequestMetadata;
};
export const deleteDocument = async ({
@ -48,9 +47,7 @@ export const deleteDocument = async ({
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
throw new Error('User not found');
}
const document = await prisma.document.findUnique({
@ -70,9 +67,7 @@ export const deleteDocument = async ({
});
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
throw new Error('Document not found');
}
const isUserOwner = document.userId === userId;
@ -80,9 +75,7 @@ export const deleteDocument = async ({
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Not allowed',
});
throw new Error('Not allowed');
}
// Handle hard or soft deleting the actual document if user has permission.
@ -137,7 +130,7 @@ type HandleDocumentOwnerDeleteOptions = {
})
| null;
user: User;
requestMetadata: ApiRequestMetadata;
requestMetadata?: RequestMetadata;
};
const handleDocumentOwnerDelete = async ({
@ -157,7 +150,8 @@ const handleDocumentOwnerDelete = async ({
data: createDocumentAuditLogData({
documentId: document.id,
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
metadata: requestMetadata,
user,
requestMetadata,
data: {
type: 'SOFT',
},
@ -183,7 +177,8 @@ const handleDocumentOwnerDelete = async ({
data: createDocumentAuditLogData({
documentId: document.id,
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
metadata: requestMetadata,
user,
requestMetadata,
data: {
type: 'HARD',
},

View File

@ -7,7 +7,7 @@ import { getDocumentWhereInput } from './get-document-by-id';
export interface DuplicateDocumentOptions {
documentId: number;
userId: number;
userId: string;
teamId?: number;
}

View File

@ -7,8 +7,7 @@ import type { FindResultResponse } from '../../types/search-params';
import { parseDocumentAuditLogData } from '../../utils/document-audit-logs';
export interface FindDocumentAuditLogsOptions {
userId: number;
teamId?: number;
userId: string;
documentId: number;
page?: number;
perPage?: number;
@ -22,7 +21,6 @@ export interface FindDocumentAuditLogsOptions {
export const findDocumentAuditLogs = async ({
userId,
teamId,
documentId,
page = 1,
perPage = 30,
@ -36,21 +34,20 @@ export const findDocumentAuditLogs = async ({
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
},
},
}
: {
userId,
teamId: null,
}),
},
},
],
},
});

View File

@ -27,7 +27,7 @@ import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-token
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
export type FindDocumentsOptions = {
userId: number;
userId: string;
teamId?: number;
templateId?: number;
source?: DocumentSource;

View File

@ -10,7 +10,7 @@ import { getTeamById } from '../team/get-team';
export type GetDocumentByIdOptions = {
documentId: number;
userId: number;
userId: string;
teamId?: number;
};
@ -58,7 +58,7 @@ export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumen
export type GetDocumentWhereInputOptions = {
documentId: number;
userId: number;
userId: string;
teamId?: number;
/**

View File

@ -14,7 +14,7 @@ import { getDocumentWhereInput } from './get-document-by-id';
export type GetDocumentWithDetailsByIdOptions = {
documentId: number;
userId: number;
userId: string;
teamId?: number;
};

View File

@ -170,7 +170,7 @@ type GetTeamCountsOption = {
teamEmail?: string;
senderIds?: number[];
currentUserEmail: string;
userId: number;
userId: string;
createdAt: Prisma.DocumentWhereInput['createdAt'];
currentTeamMemberRole?: TeamMemberRole;
search?: string;

View File

@ -124,7 +124,7 @@ type VerifyPasskeyOptions = {
/**
* The ID of the user who initiated the request.
*/
userId: number;
userId: string;
/**
* The secondary ID of the verification token.

View File

@ -1,18 +1,18 @@
import { TRPCError } from '@trpc/server';
import type { z } from 'zod';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { DocumentSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
export type MoveDocumentToTeamOptions = {
documentId: number;
teamId: number;
userId: number;
requestMetadata: ApiRequestMetadata;
userId: string;
requestMetadata?: RequestMetadata;
};
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
@ -26,6 +26,10 @@ export const moveDocumentToTeam = async ({
requestMetadata,
}: MoveDocumentToTeamOptions): Promise<TMoveDocumentToTeamResponse> => {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.findUniqueOrThrow({
where: { id: userId },
});
const document = await tx.document.findFirst({
where: {
id: documentId,
@ -35,7 +39,8 @@ export const moveDocumentToTeam = async ({
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Document not found or already associated with a team.',
});
}
@ -52,8 +57,9 @@ export const moveDocumentToTeam = async ({
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'This team does not exist, or you are not a member of this team.',
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You are not a member of this team.',
});
}
@ -62,11 +68,12 @@ export const moveDocumentToTeam = async ({
data: { teamId },
});
await tx.documentAuditLog.create({
const log = await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
documentId: updatedDocument.id,
metadata: requestMetadata,
user,
requestMetadata,
data: {
movedByUserId: userId,
fromPersonalAccount: true,

View File

@ -10,7 +10,7 @@ import {
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
@ -26,10 +26,10 @@ import { getDocumentWhereInput } from './get-document-by-id';
export type ResendDocumentOptions = {
documentId: number;
userId: number;
userId: string;
recipients: number[];
teamId?: number;
requestMetadata: ApiRequestMetadata;
requestMetadata: RequestMetadata;
};
export const resendDocument = async ({
@ -201,7 +201,8 @@ export const resendDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
metadata: requestMetadata,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,

View File

@ -8,7 +8,7 @@ import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
export type SearchDocumentsWithKeywordOptions = {
query: string;
userId: number;
userId: string;
limit?: number;
};

View File

@ -1,7 +1,7 @@
import type { z } from 'zod';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
@ -28,10 +28,10 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type SendDocumentOptions = {
documentId: number;
userId: number;
userId: string;
teamId?: number;
sendEmail?: boolean;
requestMetadata: ApiRequestMetadata;
requestMetadata?: RequestMetadata;
};
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
@ -48,6 +48,17 @@ export const sendDocument = async ({
sendEmail,
requestMetadata,
}: SendDocumentOptions): Promise<TSendDocumentResponse> => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: true,
},
});
const document = await prisma.document.findUnique({
where: {
id: documentId,
@ -187,7 +198,7 @@ export const sendDocument = async ({
userId,
documentId,
recipientId: recipient.id,
requestMetadata: requestMetadata?.requestMetadata,
requestMetadata,
},
});
}),
@ -204,7 +215,7 @@ export const sendDocument = async ({
name: 'internal.seal-document',
payload: {
documentId,
requestMetadata: requestMetadata?.requestMetadata,
requestMetadata,
},
});
@ -226,7 +237,8 @@ export const sendDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
documentId: document.id,
metadata: requestMetadata,
requestMetadata,
user,
data: {},
}),
});

View File

@ -0,0 +1,281 @@
'use server';
import { match } from 'ts-pattern';
import type { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } 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 { DocumentVisibility } from '@documenso/prisma/client';
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
export type UpdateDocumentSettingsOptions = {
userId: string;
teamId?: number;
documentId: number;
data: {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
};
requestMetadata?: RequestMetadata;
};
export const ZUpdateDocumentSettingsResponseSchema = DocumentSchema;
export type TUpdateDocumentSettingsResponse = z.infer<typeof ZUpdateDocumentSettingsResponseSchema>;
export const updateDocumentSettings = async ({
userId,
teamId,
documentId,
data,
requestMetadata,
}: UpdateDocumentSettingsOptions): Promise<TUpdateDocumentSettingsResponse> => {
if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Missing data to update',
});
}
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
team: {
select: {
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
},
},
});
if (teamId) {
const currentUserRole = document.team?.members[0]?.role;
const isDocumentOwner = document.userId === userId;
const requestedVisibility = data.visibility;
if (!isDocumentOwner) {
match(currentUserRole)
.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',
});
});
}
}
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) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isDocumentEnterprise) {
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 || documentGlobalAccessAuth === newGlobalAccessAuth;
const isGlobalActionSame =
documentGlobalActionAuth === undefined || 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,
user,
requestMetadata,
data: {
from: document.title,
to: data.title || '',
},
}),
);
}
if (!isExternalIdSame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: document.externalId,
to: data.externalId || '',
},
}),
);
}
if (!isGlobalAccessSame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: documentGlobalAccessAuth,
to: newGlobalAccessAuth,
},
}),
);
}
if (!isGlobalActionSame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: documentGlobalActionAuth,
to: newGlobalActionAuth,
},
}),
);
}
if (!isDocumentVisibilitySame) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED,
documentId,
user,
requestMetadata,
data: {
from: document.visibility,
to: data.visibility || '',
},
}),
);
}
// Early return if nothing is required.
if (auditLogs.length === 0) {
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,
authOptions,
},
});
await tx.documentAuditLog.createMany({
data: auditLogs,
});
return updatedDocument;
});
};

View File

@ -1,46 +1,23 @@
import { match } from 'ts-pattern';
import type { z } from 'zod';
'use server';
import type { Prisma } from '@prisma/client';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
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 { DocumentVisibility } from '@documenso/prisma/client';
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
export type UpdateDocumentOptions = {
userId: number;
teamId?: number;
documentId: number;
data?: {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
};
requestMetadata: ApiRequestMetadata;
data: Prisma.DocumentUpdateInput;
userId: string;
teamId?: number;
};
export const ZUpdateDocumentResponseSchema = DocumentSchema;
export type TUpdateDocumentResponse = z.infer<typeof ZUpdateDocumentResponseSchema>;
export const updateDocument = async ({
documentId,
userId,
teamId,
documentId,
data,
requestMetadata,
}: UpdateDocumentOptions): Promise<TUpdateDocumentResponse> => {
const document = await prisma.document.findFirst({
}: UpdateDocumentOptions) => {
return await prisma.document.update({
where: {
id: documentId,
...(teamId
@ -59,215 +36,8 @@ export const updateDocument = async ({
teamId: null,
}),
},
include: {
team: {
select: {
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
},
data: {
...data,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
if (teamId) {
const currentUserRole = document.team?.members[0]?.role;
const isDocumentOwner = document.userId === userId;
const requestedVisibility = data?.visibility;
if (!isDocumentOwner) {
match(currentUserRole)
.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) {
const isDocumentEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isDocumentEnterprise) {
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 || documentGlobalAccessAuth === newGlobalAccessAuth;
const isGlobalActionSame =
documentGlobalActionAuth === undefined || 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) {
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,
authOptions,
},
});
await tx.documentAuditLog.createMany({
data: auditLogs,
});
return updatedDocument;
});
};

View File

@ -6,7 +6,7 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
import { prisma } from '@documenso/prisma';
export type UpdateTitleOptions = {
userId: number;
userId: string;
teamId?: number;
documentId: number;
title: string;

View File

@ -1,148 +0,0 @@
import { z } from 'zod';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TFieldMetaSchema } 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 type { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
export interface CreateDocumentFieldsOptions {
userId: number;
teamId?: number;
documentId: number;
fields: {
recipientId: number;
type: FieldType;
pageNumber: number;
pageX: number;
pageY: number;
width: number;
height: number;
fieldMeta?: TFieldMetaSchema;
}[];
requestMetadata: ApiRequestMetadata;
}
export const ZCreateDocumentFieldsResponseSchema = z.object({
fields: z.array(FieldSchema),
});
export type TCreateDocumentFieldsResponse = z.infer<typeof ZCreateDocumentFieldsResponseSchema>;
export const createDocumentFields = async ({
userId,
teamId,
documentId,
fields,
requestMetadata,
}: CreateDocumentFieldsOptions): Promise<TCreateDocumentFieldsResponse> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,
Field: 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.Recipient.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.Field)) {
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,
};
};

View File

@ -16,7 +16,7 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
export type CreateFieldOptions = {
documentId: number;
userId: number;
userId: string;
teamId?: number;
recipientId: number;
type: FieldType;

View File

@ -1,122 +0,0 @@
import { z } from 'zod';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import type { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
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 ZCreateTemplateFieldsResponseSchema = z.object({
fields: z.array(FieldSchema),
});
export type TCreateTemplateFieldsResponse = z.infer<typeof ZCreateTemplateFieldsResponseSchema>;
export const createTemplateFields = async ({
userId,
teamId,
templateId,
fields,
}: CreateTemplateFieldsOptions): Promise<TCreateTemplateFieldsResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,
Field: true,
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'template not found',
});
}
// Field validation.
const validatedFields = fields.map((field) => {
const recipient = template.Recipient.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.Field)) {
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,
};
};

View File

@ -1,122 +0,0 @@
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';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
export interface DeleteDocumentFieldOptions {
userId: number;
teamId?: number;
fieldId: number;
requestMetadata: ApiRequestMetadata;
}
export const deleteDocumentField = async ({
userId,
teamId,
fieldId,
requestMetadata,
}: DeleteDocumentFieldOptions): Promise<void> => {
const field = await prisma.field.findFirst({
where: {
id: fieldId,
},
});
if (!field) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
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 document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: {
where: {
id: field.recipientId,
},
include: {
Field: 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',
});
}
const recipient = document.Recipient.find((recipient) => recipient.id === field.recipientId);
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient for field ${fieldId} not found`,
});
}
// Check whether the recipient associated with the field can have new fields created.
if (!canRecipientFieldsBeModified(recipient, recipient.Field)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient has already interacted with the document.',
});
}
await prisma.$transaction(async (tx) => {
const deletedField = await tx.field.delete({
where: {
id: fieldId,
},
});
// Handle field deleted audit log.
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
documentId,
metadata: requestMetadata,
data: {
fieldId: deletedField.secondaryId,
fieldRecipientEmail: recipient.email,
fieldRecipientId: deletedField.recipientId,
fieldType: deletedField.type,
},
}),
});
});
};

View File

@ -7,7 +7,7 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
export type DeleteFieldOptions = {
fieldId: number;
documentId: number;
userId: number;
userId: string;
teamId?: number;
requestMetadata?: RequestMetadata;
};

View File

@ -1,48 +0,0 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface DeleteTemplateFieldOptions {
userId: number;
teamId?: number;
fieldId: number;
}
export const deleteTemplateField = async ({
userId,
teamId,
fieldId,
}: DeleteTemplateFieldOptions): Promise<void> => {
const field = await prisma.field.findFirst({
where: {
id: fieldId,
Template: teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
},
},
});
if (!field || !field.templateId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
await prisma.field.delete({
where: {
id: fieldId,
},
});
};

View File

@ -6,7 +6,7 @@ import { FieldSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type GetFieldByIdOptions = {
userId: number;
userId: string;
teamId?: number;
fieldId: number;
documentId?: number;

View File

@ -2,35 +2,31 @@ import { prisma } from '@documenso/prisma';
export interface GetFieldsForDocumentOptions {
documentId: number;
userId: number;
teamId?: number;
userId: string;
}
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
export const getFieldsForDocument = async ({
documentId,
userId,
teamId,
}: GetFieldsForDocumentOptions) => {
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
const fields = await prisma.field.findMany({
where: {
documentId,
Document: teamId
? {
Document: {
OR: [
{
userId,
},
{
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
},
],
},
},
include: {
Signature: true,

View File

@ -0,0 +1,35 @@
import { prisma } from '@documenso/prisma';
export interface GetFieldsForTemplateOptions {
templateId: number;
userId: string;
}
export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForTemplateOptions) => {
const fields = await prisma.field.findMany({
where: {
templateId,
Template: {
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
},
},
},
},
],
},
},
orderBy: {
id: 'asc',
},
});
return fields;
};

View File

@ -16,7 +16,7 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import {
createDocumentAuditLogData,
diffFieldChanges,
@ -30,11 +30,10 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
export interface SetFieldsForDocumentOptions {
userId: number;
teamId?: number;
userId: string;
documentId: number;
fields: FieldData[];
requestMetadata: ApiRequestMetadata;
requestMetadata?: RequestMetadata;
}
export const ZSetFieldsForDocumentResponseSchema = z.object({
@ -45,7 +44,6 @@ export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocument
export const setFieldsForDocument = async ({
userId,
teamId,
documentId,
fields,
requestMetadata,
@ -53,27 +51,37 @@ export const setFieldsForDocument = async ({
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
},
},
}
: {
userId,
teamId: null,
}),
},
},
],
},
include: {
Recipient: true,
},
});
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: true,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
@ -272,7 +280,8 @@ export const setFieldsForDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
documentId: documentId,
metadata: requestMetadata,
user,
requestMetadata,
data: {
changes,
...baseAuditLog,
@ -287,7 +296,8 @@ export const setFieldsForDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
documentId: documentId,
metadata: requestMetadata,
user,
requestMetadata,
data: {
...baseAuditLog,
},
@ -315,7 +325,8 @@ export const setFieldsForDocument = async ({
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
documentId: documentId,
metadata: requestMetadata,
user,
requestMetadata,
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.Recipient?.email ?? '',

View File

@ -19,8 +19,7 @@ import { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
export type SetFieldsForTemplateOptions = {
userId: number;
teamId?: number;
userId: string;
templateId: number;
fields: {
id?: number | null;
@ -43,28 +42,26 @@ export type TSetFieldsForTemplateResponse = z.infer<typeof ZSetFieldsForTemplate
export const setFieldsForTemplate = async ({
userId,
teamId,
templateId,
fields,
}: SetFieldsForTemplateOptions): Promise<TSetFieldsForTemplateResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
},
},
}
: {
userId,
teamId: null,
}),
},
},
],
},
});

View File

@ -1,165 +0,0 @@
import { z } from 'zod';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import {
createDocumentAuditLogData,
diffFieldChanges,
} from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
export interface UpdateDocumentFieldsOptions {
userId: number;
teamId?: number;
documentId: number;
fields: {
id: number;
type?: FieldType;
pageNumber?: number;
pageX?: number;
pageY?: number;
width?: number;
height?: number;
fieldMeta?: TFieldMetaSchema;
}[];
requestMetadata: ApiRequestMetadata;
}
export const ZUpdateDocumentFieldsResponseSchema = z.object({
fields: z.array(FieldSchema),
});
export type TUpdateDocumentFieldsResponse = z.infer<typeof ZUpdateDocumentFieldsResponseSchema>;
export const updateDocumentFields = async ({
userId,
teamId,
documentId,
fields,
requestMetadata,
}: UpdateDocumentFieldsOptions): Promise<TUpdateDocumentFieldsResponse> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,
Field: 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',
});
}
const fieldsToUpdate = fields.map((field) => {
const originalField = document.Field.find((existingField) => existingField.id === field.id);
if (!originalField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Field with id ${field.id} not found`,
});
}
const recipient = document.Recipient.find(
(recipient) => recipient.id === originalField.recipientId,
);
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient attached to field ${field.id} not found`,
});
}
// Check whether the recipient associated with the field can be modified.
if (!canRecipientFieldsBeModified(recipient, document.Field)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Cannot modify a field where the recipient has already interacted with the document',
});
}
return {
originalField,
updateData: field,
recipientEmail: recipient.email,
};
});
const updatedFields = await prisma.$transaction(async (tx) => {
return await Promise.all(
fieldsToUpdate.map(async ({ originalField, updateData, recipientEmail }) => {
const updatedField = await tx.field.update({
where: {
id: updateData.id,
},
data: {
type: updateData.type,
page: updateData.pageNumber,
positionX: updateData.pageX,
positionY: updateData.pageY,
width: updateData.width,
height: updateData.height,
fieldMeta: updateData.fieldMeta,
},
});
const changes = diffFieldChanges(originalField, updatedField);
// Handle field updated audit log.
if (changes.length > 0) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
documentId: documentId,
metadata: requestMetadata,
data: {
fieldId: updatedField.secondaryId,
fieldRecipientEmail: recipientEmail,
fieldRecipientId: updatedField.recipientId,
fieldType: updatedField.type,
changes,
},
}),
});
}
return updatedField;
}),
);
});
return {
fields: updatedFields,
};
};

View File

@ -9,7 +9,7 @@ import { createDocumentAuditLogData, diffFieldChanges } from '../../utils/docume
export type UpdateFieldOptions = {
fieldId: number;
documentId: number;
userId: number;
userId: string;
teamId?: number;
recipientId?: number;
type?: FieldType;

View File

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

View File

@ -3,13 +3,13 @@ import sharp from 'sharp';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
export type SetAvatarImageOptions = {
userId: number;
userId: string;
teamId?: number | null;
bytes?: string | null;
requestMetadata: ApiRequestMetadata;
requestMetadata?: RequestMetadata;
};
export const setAvatarImage = async ({

View File

@ -14,7 +14,7 @@ type TimeConstants = typeof timeConstants & {
};
type CreateApiTokenInput = {
userId: number;
userId: string;
teamId?: number;
tokenName: string;
expiresIn: string | null;

View File

@ -3,7 +3,7 @@ import { TeamMemberRole } from '@documenso/prisma/client';
export type DeleteTokenByIdOptions = {
id: number;
userId: number;
userId: string;
teamId?: number;
};

View File

@ -3,7 +3,7 @@ import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
export type GetUserTokensOptions = {
userId: number;
userId: string;
teamId: number;
};

View File

@ -1,7 +1,7 @@
import { prisma } from '@documenso/prisma';
export type GetUserTokensOptions = {
userId: number;
userId: string;
};
export const getUserTokens = async ({ userId }: GetUserTokensOptions) => {

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