Compare commits

...

6 Commits

Author SHA1 Message Date
dcaccb65f2 v1.9.0-rc.8 2025-01-13 10:21:05 +11:00
723e1b4ea2 fix: include all template meta in findTemplates 2025-01-13 09:34:23 +11:00
08a69c6168 fix: pending document edit (#1580)
Fix issue where you cannot edit a pending document when there is a CCer
recipient.
2025-01-11 22:31:59 +11:00
948d9c24cf fix: broken direct template webhook (#1579) 2025-01-11 21:42:33 +11:00
ebbe922982 feat: add template and field endpoints (#1572) 2025-01-11 15:33:20 +11:00
6520bbd5e3 Update FEATURES
chore: typo
2025-01-10 14:25:39 +01:00
98 changed files with 3945 additions and 1410 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@documenso/web",
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.8",
"private": true,
"license": "AGPL-3.0",
"scripts": {

View File

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

View File

@ -76,7 +76,6 @@ 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

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

View File

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

View File

@ -15,11 +15,7 @@ export type DownloadAuditLogButtonProps = {
documentId: number;
};
export const DownloadAuditLogButton = ({
className,
teamId,
documentId,
}: DownloadAuditLogButtonProps) => {
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();
@ -28,7 +24,7 @@ export const DownloadAuditLogButton = ({
const onDownloadAuditLogsClick = async () => {
try {
const { url } = await downloadAuditLogs({ teamId, documentId });
const { url } = await downloadAuditLogs({ 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, teamId });
const { url } = await downloadCertificate({ documentId });
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, teamId: team?.id });
await resendDocument({ documentId: document.id, recipients });
toast({
title: _(msg`Document re-sent`),

View File

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

View File

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

View File

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

View File

@ -61,7 +61,6 @@ 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,15 +23,12 @@ 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();
@ -43,9 +40,7 @@ export const PublicTemplatesDataTable = () => {
} | null>(null);
const { data, isInitialLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
{
teamId: team?.id,
},
{},
{
keepPreviousData: true,
},

View File

@ -62,7 +62,6 @@ export const EditTemplateForm = ({
const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
{
templateId: initialTemplate.id,
teamId: initialTemplate.teamId || undefined,
},
{
initialData: initialTemplate,
@ -104,19 +103,6 @@ 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) => {
@ -129,7 +115,7 @@ export const EditTemplateForm = ({
},
});
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation({
const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateById.setData(
@ -141,28 +127,10 @@ 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,
@ -196,16 +164,16 @@ export const EditTemplateForm = ({
) => {
try {
await Promise.all([
setSigningOrderForTemplate({
updateTemplateSettings({
templateId: template.id,
teamId: team?.id,
signingOrder: data.signingOrder,
meta: {
signingOrder: data.signingOrder,
},
}),
addTemplateSigners({
setRecipients({
templateId: template.id,
teamId: team?.id,
signers: data.signers,
recipients: data.signers,
}),
]);
@ -229,10 +197,11 @@ export const EditTemplateForm = ({
fields: data.fields,
});
await updateTypedSignature({
await updateTemplateSettings({
templateId: template.id,
teamId: team?.id,
typedSignatureEnabled: data.typedSignatureEnabled,
meta: {
typedSignatureEnabled: data.typedSignatureEnabled,
},
});
// Clear all field data from localStorage

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,8 +47,6 @@ 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(),
@ -120,8 +118,6 @@ export function UseTemplateDialog({
const [open, setOpen] = useState(false);
const team = useOptionalCurrentTeam();
const form = useForm<TAddRecipientsForNewDocumentSchema>({
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
defaultValues: {
@ -163,7 +159,6 @@ export function UseTemplateDialog({
const { id } = await createDocumentFromTemplate({
templateId,
teamId: team?.id,
recipients: data.recipients,
distributeDocument: data.distributeDocument,
customDocumentDataId,

View File

@ -66,6 +66,7 @@ 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,6 +8,7 @@ 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';
@ -47,6 +48,10 @@ 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}>
@ -61,7 +66,9 @@ export default async function AuthenticatedTeamsLayout({
<Header user={user} teams={teams} />
<TeamProvider team={team}>
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
<TrpcProvider headers={trpcHeaders}>
<main className="mt-8 pb-8 md:mt-12 md:pb-12">{children}</main>
</TrpcProvider>
</TeamProvider>
<RefreshOnFocus />

View File

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

View File

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

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 }),
createTrpcContext({ req, res, requestSource: 'apiV2' }),
onError: ({ error, path }: { error: TRPCError; path?: string }) => {
// Always log the error for now.
console.error(error.message);

6
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.8",
"workspaces": [
"apps/*",
"packages/*"
@ -133,7 +133,7 @@
},
"apps/web": {
"name": "@documenso/web",
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.8",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/api": "*",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.9.0-rc.7",
"version": "1.9.0-rc.8",
"scripts": {
"build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web",

View File

@ -16,8 +16,7 @@ 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 } from '@documenso/lib/server-only/document/update-document';
import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings';
import { updateDocument as updateDocumentSettings } from '@documenso/lib/server-only/document/update-document';
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';
@ -26,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 { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
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';
@ -54,6 +53,7 @@ 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,
@ -98,13 +98,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
const recipients = await getRecipientsForDocument({
documentId: Number(documentId),
teamId: team?.id,
userId: user.id,
teamId: team?.id,
});
const fields = await getFieldsForDocument({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
});
const parsedMetaFields = fields.map((field) => {
@ -209,7 +210,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
try {
@ -232,6 +233,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
id: document.id,
userId: user.id,
teamId: team?.id,
requestMetadata: metadata,
});
return {
@ -248,7 +250,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createDocument: authenticatedMiddleware(async (args, user, team) => {
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body } = args;
try {
@ -316,12 +318,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
teamId: team?.id,
formValues: body.formValues,
documentDataId: documentData.id,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
subject: body.meta.subject,
message: body.meta.message,
timezone,
@ -332,7 +335,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
typedSignatureEnabled: body.meta.typedSignatureEnabled,
distributionMethod: body.meta.distributionMethod,
emailSettings: body.meta.emailSettings,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
if (body.authOptions) {
@ -343,16 +346,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
data: {
...body.authOptions,
},
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
}
const { recipients } = await setRecipientsForDocument({
const { recipients } = await setDocumentRecipients({
userId: user.id,
teamId: team?.id,
documentId: document.id,
recipients: body.recipients,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
return {
@ -453,7 +456,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
@ -517,8 +520,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
}
@ -528,7 +532,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
}
@ -550,7 +554,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
};
}),
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ email: user.email, teamId: team?.id });
@ -579,6 +583,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
@ -621,7 +626,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
}
@ -642,7 +647,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
};
}),
sendDocument: authenticatedMiddleware(async (args, user, team) => {
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const { sendEmail, sendCompletionEmails } = args.body;
@ -678,12 +683,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
emailSettings: {
...emailSettings,
documentCompleted: sendCompletionEmails,
ownerDocumentCompleted: sendCompletionEmails,
},
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
}
@ -692,7 +698,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
userId: user.id,
teamId: team?.id,
sendEmail,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
return {
@ -716,7 +722,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
resendDocument: authenticatedMiddleware(async (args, user, team) => {
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const { recipients } = args.body;
@ -726,7 +732,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
documentId: Number(documentId),
recipients,
teamId: team?.id,
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
return {
@ -745,7 +751,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),
createRecipient: authenticatedMiddleware(async (args, user, team) => {
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { id: documentId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
@ -791,7 +797,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
try {
const { recipients: newRecipients } = await setRecipientsForDocument({
const { recipients: newRecipients } = await setDocumentRecipients({
documentId: Number(documentId),
userId: user.id,
teamId: team?.id,
@ -809,7 +815,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
actionAuth: authOptions?.actionAuth ?? null,
},
],
requestMetadata: extractNextApiRequestMetadata(args.req),
requestMetadata: metadata,
});
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
@ -1574,3 +1580,39 @@ 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,6 +2,8 @@ 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 = <
@ -13,7 +15,12 @@ export const authenticatedMiddleware = <
body: unknown;
},
>(
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
handler: (
args: T,
user: User,
team: Team | null | undefined,
options: { metadata: ApiRequestMetadata },
) => Promise<R>,
) => {
return async (args: T) => {
try {
@ -36,7 +43,18 @@ export const authenticatedMiddleware = <
});
}
return await handler(args, apiToken.user, apiToken.team);
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 });
} catch (err) {
console.log({ err: err });

View File

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

View File

@ -1,7 +1,7 @@
'use server';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import {
createDocumentAuditLogData,
diffDocumentMetaChanges,
@ -13,6 +13,8 @@ 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;
@ -25,18 +27,18 @@ export type CreateDocumentMetaOptions = {
distributionMethod?: DocumentDistributionMethod;
typedSignatureEnabled?: boolean;
language?: SupportedLanguageCodes;
userId: number;
requestMetadata: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const upsertDocumentMeta = async ({
userId,
teamId,
subject,
message,
timezone,
dateFormat,
documentId,
password,
userId,
redirectUrl,
signingOrder,
emailSettings,
@ -45,34 +47,24 @@ 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,
OR: [
{
userId: user.id,
},
{
team: {
members: {
some: {
userId: user.id,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
},
},
],
}
: {
userId,
teamId: null,
}),
},
include: {
documentMeta: true,
@ -120,8 +112,7 @@ export const upsertDocumentMeta = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED,
documentId,
user,
requestMetadata,
metadata: 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 { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
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 { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
@ -27,7 +27,7 @@ export type CreateDocumentOptions = {
formValues?: Record<string, string | number | boolean>;
normalizePdf?: boolean;
timezone?: string;
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const ZCreateDocumentResponseSchema = DocumentSchema;
@ -162,8 +162,7 @@ export const createDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id,
user,
requestMetadata,
metadata: requestMetadata,
data: {
title,
source: {

View File

@ -20,9 +20,10 @@ 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 { RequestMetadata } from '../../universal/extract-request-metadata';
import type { ApiRequestMetadata } 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';
@ -31,7 +32,7 @@ export type DeleteDocumentOptions = {
id: number;
userId: number;
teamId?: number;
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const deleteDocument = async ({
@ -47,7 +48,9 @@ export const deleteDocument = async ({
});
if (!user) {
throw new Error('User not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
const document = await prisma.document.findUnique({
@ -67,7 +70,9 @@ export const deleteDocument = async ({
});
if (!document || (teamId !== undefined && teamId !== document.teamId)) {
throw new Error('Document not found');
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
const isUserOwner = document.userId === userId;
@ -75,7 +80,9 @@ export const deleteDocument = async ({
const userRecipient = document.Recipient.find((recipient) => recipient.email === user.email);
if (!isUserOwner && !isUserTeamMember && !userRecipient) {
throw new Error('Not allowed');
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Not allowed',
});
}
// Handle hard or soft deleting the actual document if user has permission.
@ -130,7 +137,7 @@ type HandleDocumentOwnerDeleteOptions = {
})
| null;
user: User;
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
const handleDocumentOwnerDelete = async ({
@ -150,8 +157,7 @@ const handleDocumentOwnerDelete = async ({
data: createDocumentAuditLogData({
documentId: document.id,
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
user,
requestMetadata,
metadata: requestMetadata,
data: {
type: 'SOFT',
},
@ -177,8 +183,7 @@ const handleDocumentOwnerDelete = async ({
data: createDocumentAuditLogData({
documentId: document.id,
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
user,
requestMetadata,
metadata: requestMetadata,
data: {
type: 'HARD',
},

View File

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

View File

@ -1,10 +1,10 @@
import { TRPCError } from '@trpc/server';
import type { z } from 'zod';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { ApiRequestMetadata } 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';
@ -12,7 +12,7 @@ export type MoveDocumentToTeamOptions = {
documentId: number;
teamId: number;
userId: number;
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const ZMoveDocumentToTeamResponseSchema = DocumentSchema;
@ -26,10 +26,6 @@ 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,
@ -39,8 +35,7 @@ export const moveDocumentToTeam = async ({
});
if (!document) {
throw new TRPCError({
code: 'NOT_FOUND',
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found or already associated with a team.',
});
}
@ -57,9 +52,8 @@ export const moveDocumentToTeam = async ({
});
if (!team) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You are not a member of this team.',
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'This team does not exist, or you are not a member of this team.',
});
}
@ -68,12 +62,11 @@ export const moveDocumentToTeam = async ({
data: { teamId },
});
const log = await tx.documentAuditLog.create({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
documentId: updatedDocument.id,
user,
requestMetadata,
metadata: 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 { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { ApiRequestMetadata } 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';
@ -29,7 +29,7 @@ export type ResendDocumentOptions = {
userId: number;
recipients: number[];
teamId?: number;
requestMetadata: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const resendDocument = async ({
@ -201,8 +201,7 @@ export const resendDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
metadata: requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,

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 { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { ApiRequestMetadata } 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';
@ -31,7 +31,7 @@ export type SendDocumentOptions = {
userId: number;
teamId?: number;
sendEmail?: boolean;
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const ZSendDocumentResponseSchema = DocumentSchema.extend({
@ -48,17 +48,6 @@ 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,
@ -198,7 +187,7 @@ export const sendDocument = async ({
userId,
documentId,
recipientId: recipient.id,
requestMetadata,
requestMetadata: requestMetadata?.requestMetadata,
},
});
}),
@ -215,7 +204,7 @@ export const sendDocument = async ({
name: 'internal.seal-document',
payload: {
documentId,
requestMetadata,
requestMetadata: requestMetadata?.requestMetadata,
},
});
@ -237,8 +226,7 @@ export const sendDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
documentId: document.id,
requestMetadata,
user,
metadata: requestMetadata,
data: {},
}),
});

View File

@ -1,281 +0,0 @@
'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: number;
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,23 +1,46 @@
'use server';
import type { Prisma } from '@prisma/client';
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 { 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 = {
documentId: number;
data: Prisma.DocumentUpdateInput;
userId: number;
teamId?: number;
documentId: number;
data?: {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility | null;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
};
requestMetadata: ApiRequestMetadata;
};
export const ZUpdateDocumentResponseSchema = DocumentSchema;
export type TUpdateDocumentResponse = z.infer<typeof ZUpdateDocumentResponseSchema>;
export const updateDocument = async ({
documentId,
userId,
teamId,
documentId,
data,
}: UpdateDocumentOptions) => {
return await prisma.document.update({
requestMetadata,
}: UpdateDocumentOptions): Promise<TUpdateDocumentResponse> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
@ -36,8 +59,215 @@ export const updateDocument = async ({
teamId: null,
}),
},
data: {
...data,
include: {
team: {
select: {
members: {
where: {
userId,
},
select: {
role: true,
},
},
},
},
},
});
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

@ -0,0 +1,148 @@
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

@ -0,0 +1,122 @@
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

@ -0,0 +1,122 @@
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

@ -0,0 +1,48 @@
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

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

View File

@ -1,35 +0,0 @@
import { prisma } from '@documenso/prisma';
export interface GetFieldsForTemplateOptions {
templateId: number;
userId: number;
}
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 { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import {
createDocumentAuditLogData,
diffFieldChanges,
@ -31,9 +31,10 @@ import { canRecipientFieldsBeModified } from '../../utils/recipients';
export interface SetFieldsForDocumentOptions {
userId: number;
teamId?: number;
documentId: number;
fields: FieldData[];
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
}
export const ZSetFieldsForDocumentResponseSchema = z.object({
@ -44,6 +45,7 @@ export type TSetFieldsForDocumentResponse = z.infer<typeof ZSetFieldsForDocument
export const setFieldsForDocument = async ({
userId,
teamId,
documentId,
fields,
requestMetadata,
@ -51,37 +53,27 @@ export const setFieldsForDocument = async ({
const document = await prisma.document.findFirst({
where: {
id: documentId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
...(teamId
? {
team: {
id: teamId,
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',
@ -280,8 +272,7 @@ export const setFieldsForDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
documentId: documentId,
user,
requestMetadata,
metadata: requestMetadata,
data: {
changes,
...baseAuditLog,
@ -296,8 +287,7 @@ export const setFieldsForDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
documentId: documentId,
user,
requestMetadata,
metadata: requestMetadata,
data: {
...baseAuditLog,
},
@ -325,8 +315,7 @@ export const setFieldsForDocument = async ({
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
documentId: documentId,
user,
requestMetadata,
metadata: requestMetadata,
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.Recipient?.email ?? '',

View File

@ -20,6 +20,7 @@ import { FieldSchema } from '@documenso/prisma/generated/zod';
export type SetFieldsForTemplateOptions = {
userId: number;
teamId?: number;
templateId: number;
fields: {
id?: number | null;
@ -42,26 +43,28 @@ 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,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
},
},
],
}
: {
userId,
teamId: null,
}),
},
});

View File

@ -0,0 +1,165 @@
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

@ -0,0 +1,129 @@
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 { RequestMetadata } from '../../universal/extract-request-metadata';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
export type SetAvatarImageOptions = {
userId: number;
teamId?: number | null;
bytes?: string | null;
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const setAvatarImage = async ({

View File

@ -0,0 +1,167 @@
import { 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 { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid } from '@documenso/lib/universal/id';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { ZRecipientBaseResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface CreateDocumentRecipientsOptions {
userId: number;
teamId?: number;
documentId: number;
recipients: {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes | null;
actionAuth?: TRecipientActionAuthTypes | null;
}[];
requestMetadata: ApiRequestMetadata;
}
export const ZCreateDocumentRecipientsResponseSchema = z.object({
recipients: ZRecipientBaseResponseSchema.array(),
});
export type TCreateDocumentRecipientsResponse = z.infer<
typeof ZCreateDocumentRecipientsResponseSchema
>;
export const createDocumentRecipients = async ({
userId,
teamId,
documentId,
recipients: recipientsToCreate,
requestMetadata,
}: CreateDocumentRecipientsOptions): Promise<TCreateDocumentRecipientsResponse> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: 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 recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth) {
const isEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isEnterprise) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
}));
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
const existingRecipient = document.Recipient.find(
(existingRecipient) => existingRecipient.email === newRecipient.email,
);
return existingRecipient !== undefined;
});
if (duplicateRecipients.length > 0) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
});
}
const createdRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
normalizedRecipients.map(async (recipient) => {
const authOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth || null,
actionAuth: recipient.actionAuth || null,
});
const createdRecipient = await tx.recipient.create({
data: {
documentId,
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
authOptions,
},
});
// Handle recipient created audit log.
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
documentId: documentId,
metadata: requestMetadata,
data: {
recipientEmail: createdRecipient.email,
recipientName: createdRecipient.name,
recipientId: createdRecipient.id,
recipientRole: createdRecipient.role,
accessAuth: recipient.accessAuth || undefined,
actionAuth: recipient.actionAuth || undefined,
},
}),
});
return createdRecipient;
}),
);
});
return {
recipients: createdRecipients,
};
};

View File

@ -0,0 +1,139 @@
import { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { ZRecipientBaseResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface CreateTemplateRecipientsOptions {
userId: number;
teamId?: number;
templateId: number;
recipients: {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes | null;
actionAuth?: TRecipientActionAuthTypes | null;
}[];
}
export const ZCreateTemplateRecipientsResponseSchema = z.object({
recipients: ZRecipientBaseResponseSchema.array(),
});
export type TCreateTemplateRecipientsResponse = z.infer<
typeof ZCreateTemplateRecipientsResponseSchema
>;
export const createTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients: recipientsToCreate,
}: CreateTemplateRecipientsOptions): Promise<TCreateTemplateRecipientsResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth) {
const isEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isEnterprise) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
}));
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
const existingRecipient = template.Recipient.find(
(existingRecipient) => existingRecipient.email === newRecipient.email,
);
return existingRecipient !== undefined;
});
if (duplicateRecipients.length > 0) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
});
}
const createdRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
normalizedRecipients.map(async (recipient) => {
const authOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth || null,
actionAuth: recipient.actionAuth || null,
});
const createdRecipient = await tx.recipient.create({
data: {
templateId,
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
authOptions,
},
});
return createdRecipient;
}),
);
});
return {
recipients: createdRecipients,
};
};

View File

@ -0,0 +1,161 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { 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 { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface DeleteDocumentRecipientOptions {
userId: number;
teamId?: number;
recipientId: number;
requestMetadata: ApiRequestMetadata;
}
export const deleteDocumentRecipient = async ({
userId,
teamId,
recipientId,
requestMetadata,
}: DeleteDocumentRecipientOptions): Promise<void> => {
const document = await prisma.document.findFirst({
where: {
Recipient: {
some: {
id: recipientId,
},
},
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
documentMeta: true,
team: true,
Recipient: {
where: {
id: recipientId,
},
},
},
});
const user = await prisma.user.findFirst({
where: {
id: userId,
},
select: {
id: true,
name: true,
email: 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',
});
}
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
const recipientToDelete = document.Recipient[0];
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
await prisma.$transaction(async (tx) => {
await tx.recipient.delete({
where: {
id: recipientId,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
documentId: document.id,
metadata: requestMetadata,
data: {
recipientEmail: recipientToDelete.email,
recipientName: recipientToDelete.name,
recipientId: recipientToDelete.id,
recipientRole: recipientToDelete.role,
},
}),
});
});
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientRemoved;
// Send email to deleted recipient.
if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) {
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(RecipientRemovedFromDocumentTemplate, {
documentName: document.title,
inviterName: document.team?.name || user.name || undefined,
assetBaseUrl,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
await mailer.sendMail({
to: {
address: recipientToDelete.email,
name: recipientToDelete.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`You have been removed from a document`),
html,
text,
});
}
};

View File

@ -0,0 +1,67 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface DeleteTemplateRecipientOptions {
userId: number;
teamId?: number;
recipientId: number;
}
export const deleteTemplateRecipient = async ({
userId,
teamId,
recipientId,
}: DeleteTemplateRecipientOptions): Promise<void> => {
const template = await prisma.template.findFirst({
where: {
Recipient: {
some: {
id: recipientId,
},
},
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: {
where: {
id: recipientId,
},
},
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientToDelete = template.Recipient[0];
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
await prisma.recipient.delete({
where: {
id: recipientId,
},
});
};

View File

@ -29,25 +29,21 @@ export const getRecipientById = async ({
const recipient = await prisma.recipient.findFirst({
where: {
id: recipientId,
Document: {
OR: [
teamId === undefined
? {
userId,
teamId: null,
}
: {
teamId,
team: {
members: {
some: {
userId,
},
},
Document: teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
],
},
},
}
: {
userId,
teamId: null,
},
},
include: {
Field: true,

View File

@ -14,23 +14,21 @@ export const getRecipientsForDocument = async ({
const recipients = await prisma.recipient.findMany({
where: {
documentId,
Document: {
OR: [
{
userId,
},
{
teamId,
Document: teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
},
],
},
},
orderBy: {
id: 'asc',

View File

@ -3,31 +3,32 @@ import { prisma } from '@documenso/prisma';
export interface GetRecipientsForTemplateOptions {
templateId: number;
userId: number;
teamId?: number;
}
export const getRecipientsForTemplate = async ({
templateId,
userId,
teamId,
}: GetRecipientsForTemplateOptions) => {
const recipients = await prisma.recipient.findMany({
where: {
templateId,
Template: {
OR: [
{
userId,
},
{
Template: teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
},
],
},
},
orderBy: {
id: 'asc',

View File

@ -7,11 +7,12 @@ import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-ent
import { mailer } from '@documenso/email/mailer';
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid } from '@documenso/lib/universal/id';
import {
createDocumentAuditLogData,
@ -33,29 +34,27 @@ import { canRecipientBeModified } from '../../utils/recipients';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
export interface SetRecipientsForDocumentOptions {
export interface SetDocumentRecipientsOptions {
userId: number;
teamId?: number;
documentId: number;
recipients: RecipientData[];
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
}
export const ZSetRecipientsForDocumentResponseSchema = z.object({
export const ZSetDocumentRecipientsResponseSchema = z.object({
recipients: RecipientSchema.array(),
});
export type TSetRecipientsForDocumentResponse = z.infer<
typeof ZSetRecipientsForDocumentResponseSchema
>;
export type TSetDocumentRecipientsResponse = z.infer<typeof ZSetDocumentRecipientsResponseSchema>;
export const setRecipientsForDocument = async ({
export const setDocumentRecipients = async ({
userId,
teamId,
documentId,
recipients,
requestMetadata,
}: SetRecipientsForDocumentOptions): Promise<TSetRecipientsForDocumentResponse> => {
}: SetDocumentRecipientsOptions): Promise<TSetDocumentRecipientsResponse> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
@ -167,10 +166,10 @@ export const setRecipientsForDocument = async ({
linkedRecipients.map(async (recipient) => {
let authOptions = ZRecipientAuthOptionsSchema.parse(recipient._persisted?.authOptions);
if (recipient.actionAuth !== undefined) {
if (recipient.actionAuth !== undefined || recipient.accessAuth !== undefined) {
authOptions = createRecipientAuthOptions({
accessAuth: authOptions.accessAuth,
actionAuth: recipient.actionAuth,
accessAuth: recipient.accessAuth || authOptions.accessAuth,
actionAuth: recipient.actionAuth || authOptions.actionAuth,
});
}
@ -236,8 +235,7 @@ export const setRecipientsForDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
documentId: documentId,
user,
requestMetadata,
metadata: requestMetadata,
data: {
changes,
...baseAuditLog,
@ -252,10 +250,10 @@ export const setRecipientsForDocument = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
documentId: documentId,
user,
requestMetadata,
metadata: requestMetadata,
data: {
...baseAuditLog,
accessAuth: recipient.accessAuth || undefined,
actionAuth: recipient.actionAuth || undefined,
},
}),
@ -282,8 +280,7 @@ export const setRecipientsForDocument = async ({
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
documentId: documentId,
user,
requestMetadata,
metadata: requestMetadata,
data: {
recipientEmail: recipient.email,
recipientName: recipient.name,
@ -368,17 +365,22 @@ type RecipientData = {
name: string;
role: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes | null;
actionAuth?: TRecipientActionAuthTypes | null;
};
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
const newRecipientActionAuth = newRecipientData.actionAuth || null;
return (
recipient.email !== newRecipientData.email ||
recipient.name !== newRecipientData.name ||
recipient.role !== newRecipientData.role ||
recipient.signingOrder !== newRecipientData.signingOrder ||
authOptions.actionAuth !== newRecipientData.actionAuth
authOptions.accessAuth !== newRecipientAccessAuth ||
authOptions.actionAuth !== newRecipientActionAuth
);
};

View File

@ -18,7 +18,7 @@ import {
import { nanoid } from '../../universal/id';
import { createRecipientAuthOptions } from '../../utils/document-auth';
export type SetRecipientsForTemplateOptions = {
export type SetTemplateRecipientsOptions = {
userId: number;
teamId?: number;
templateId: number;
@ -32,37 +32,36 @@ export type SetRecipientsForTemplateOptions = {
}[];
};
export const ZSetRecipientsForTemplateResponseSchema = z.object({
export const ZSetTemplateRecipientsResponseSchema = z.object({
recipients: RecipientSchema.array(),
});
export type TSetRecipientsForTemplateResponse = z.infer<
typeof ZSetRecipientsForTemplateResponseSchema
>;
export type TSetTemplateRecipientsResponse = z.infer<typeof ZSetTemplateRecipientsResponseSchema>;
export const setRecipientsForTemplate = async ({
export const setTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients,
}: SetRecipientsForTemplateOptions): Promise<TSetRecipientsForTemplateResponse> => {
}: SetTemplateRecipientsOptions): Promise<TSetTemplateRecipientsResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
},
},
],
}
: {
userId,
teamId: null,
}),
},
include: {
directLink: true,

View File

@ -0,0 +1,249 @@
import { 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 { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import {
createDocumentAuditLogData,
diffRecipientChanges,
} from '@documenso/lib/utils/document-audit-logs';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { ZRecipientResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientBeModified } from '../../utils/recipients';
export interface UpdateDocumentRecipientsOptions {
userId: number;
teamId?: number;
documentId: number;
recipients: RecipientData[];
requestMetadata: ApiRequestMetadata;
}
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
recipients: ZRecipientResponseSchema.array(),
});
export type TUpdateDocumentRecipientsResponse = z.infer<
typeof ZUpdateDocumentRecipientsResponseSchema
>;
export const updateDocumentRecipients = async ({
userId,
teamId,
documentId,
recipients,
requestMetadata,
}: UpdateDocumentRecipientsOptions): Promise<TUpdateDocumentRecipientsResponse> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Field: true,
Recipient: 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 recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth) {
const isEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isEnterprise) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
const recipientsToUpdate = recipients.map((recipient) => {
const originalRecipient = document.Recipient.find(
(existingRecipient) => existingRecipient.id === recipient.id,
);
if (!originalRecipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Recipient with id ${recipient.id} not found`,
});
}
const duplicateRecipientWithSameEmail = document.Recipient.find(
(existingRecipient) =>
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
);
if (duplicateRecipientWithSameEmail) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
});
}
if (
hasRecipientBeenChanged(originalRecipient, recipient) &&
!canRecipientBeModified(originalRecipient, document.Field)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Cannot modify a recipient who has already interacted with the document',
});
}
return {
originalRecipient,
updateData: recipient,
};
});
const updatedRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
recipientsToUpdate.map(async ({ originalRecipient, updateData }) => {
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
if (updateData.actionAuth !== undefined || updateData.accessAuth !== undefined) {
authOptions = createRecipientAuthOptions({
accessAuth: updateData.accessAuth || authOptions.accessAuth,
actionAuth: updateData.actionAuth || authOptions.actionAuth,
});
}
const mergedRecipient = {
...originalRecipient,
...updateData,
};
const updatedRecipient = await tx.recipient.update({
where: {
id: originalRecipient.id,
documentId,
},
data: {
name: mergedRecipient.name,
email: mergedRecipient.email,
role: mergedRecipient.role,
signingOrder: mergedRecipient.signingOrder,
documentId,
sendStatus:
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
mergedRecipient.role === RecipientRole.CC
? SigningStatus.SIGNED
: SigningStatus.NOT_SIGNED,
authOptions,
},
include: {
Field: true,
},
});
// Clear all fields if the recipient role is changed to a type that cannot have fields.
if (
originalRecipient.role !== updatedRecipient.role &&
(updatedRecipient.role === RecipientRole.CC ||
updatedRecipient.role === RecipientRole.VIEWER)
) {
await tx.field.deleteMany({
where: {
recipientId: updatedRecipient.id,
},
});
}
const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
// Handle recipient updated audit log.
if (changes.length > 0) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
documentId: documentId,
metadata: requestMetadata,
data: {
recipientEmail: updatedRecipient.email,
recipientName: updatedRecipient.name,
recipientId: updatedRecipient.id,
recipientRole: updatedRecipient.role,
changes,
},
}),
});
}
return updatedRecipient;
}),
);
});
return {
recipients: updatedRecipients,
};
};
/**
* If you change this you MUST update the `hasRecipientBeenChanged` function.
*/
type RecipientData = {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes | null;
actionAuth?: TRecipientActionAuthTypes | null;
};
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
const newRecipientActionAuth = newRecipientData.actionAuth || null;
return (
recipient.email !== newRecipientData.email ||
recipient.name !== newRecipientData.name ||
recipient.role !== newRecipientData.role ||
recipient.signingOrder !== newRecipientData.signingOrder ||
authOptions.accessAuth !== newRecipientAccessAuth ||
authOptions.actionAuth !== newRecipientActionAuth
);
};

View File

@ -0,0 +1,185 @@
import { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { ZRecipientResponseSchema } from '@documenso/trpc/server/recipient-router/schema';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface UpdateTemplateRecipientsOptions {
userId: number;
teamId?: number;
templateId: number;
recipients: {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes | null;
actionAuth?: TRecipientActionAuthTypes | null;
}[];
}
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
recipients: ZRecipientResponseSchema.array(),
});
export type TUpdateTemplateRecipientsResponse = z.infer<
typeof ZUpdateTemplateRecipientsResponseSchema
>;
export const updateTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients,
}: UpdateTemplateRecipientsOptions): Promise<TUpdateTemplateRecipientsResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth) {
const isEnterprise = await isUserEnterprise({
userId,
teamId,
});
if (!isEnterprise) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
}
const recipientsToUpdate = recipients.map((recipient) => {
const originalRecipient = template.Recipient.find(
(existingRecipient) => existingRecipient.id === recipient.id,
);
if (!originalRecipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Recipient with id ${recipient.id} not found`,
});
}
const duplicateRecipientWithSameEmail = template.Recipient.find(
(existingRecipient) =>
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
);
if (duplicateRecipientWithSameEmail) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
});
}
return {
originalRecipient,
recipientUpdateData: recipient,
};
});
const updatedRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
recipientsToUpdate.map(async ({ originalRecipient, recipientUpdateData }) => {
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
if (
recipientUpdateData.actionAuth !== undefined ||
recipientUpdateData.accessAuth !== undefined
) {
authOptions = createRecipientAuthOptions({
accessAuth: recipientUpdateData.accessAuth || authOptions.accessAuth,
actionAuth: recipientUpdateData.actionAuth || authOptions.actionAuth,
});
}
const mergedRecipient = {
...originalRecipient,
...recipientUpdateData,
};
const updatedRecipient = await tx.recipient.update({
where: {
id: originalRecipient.id,
templateId,
},
data: {
name: mergedRecipient.name,
email: mergedRecipient.email,
role: mergedRecipient.role,
signingOrder: mergedRecipient.signingOrder,
templateId,
sendStatus:
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
mergedRecipient.role === RecipientRole.CC
? SigningStatus.SIGNED
: SigningStatus.NOT_SIGNED,
authOptions,
},
include: {
Field: true,
},
});
// Clear all fields if the recipient role is changed to a type that cannot have fields.
if (
originalRecipient.role !== updatedRecipient.role &&
(updatedRecipient.role === RecipientRole.CC ||
updatedRecipient.role === RecipientRole.VIEWER)
) {
await tx.field.deleteMany({
where: {
recipientId: updatedRecipient.id,
},
});
}
return updatedRecipient;
}),
);
});
return {
recipients: updatedRecipients,
};
};

View File

@ -33,7 +33,7 @@ import type { TRecipientActionAuthTypes } from '../../types/document-auth';
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import { ZFieldMetaSchema } from '../../types/field-meta';
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import {
@ -55,7 +55,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
directTemplateExternalId?: string;
signedFieldValues: TSignFieldWithTokenMutationSchema[];
templateUpdatedAt: Date;
requestMetadata: RequestMetadata;
requestMetadata: ApiRequestMetadata;
user?: {
id: number;
name?: string;
@ -454,7 +454,7 @@ export const createDocumentFromDirectTemplate = async ({
name: user?.name,
email: directRecipientEmail,
},
requestMetadata,
metadata: requestMetadata,
data: {
title: document.title,
source: {
@ -472,7 +472,7 @@ export const createDocumentFromDirectTemplate = async ({
name: user?.name,
email: directRecipientEmail,
},
requestMetadata,
metadata: requestMetadata,
data: {
recipientEmail: createdDirectRecipient.email,
recipientId: createdDirectRecipient.id,
@ -490,7 +490,7 @@ export const createDocumentFromDirectTemplate = async ({
name: user?.name,
email: directRecipientEmail,
},
requestMetadata,
metadata: requestMetadata,
data: {
recipientEmail: createdDirectRecipient.email,
recipientId: createdDirectRecipient.id,
@ -535,7 +535,7 @@ export const createDocumentFromDirectTemplate = async ({
name: user?.name,
email: directRecipientEmail,
},
requestMetadata,
metadata: requestMetadata,
data: {
recipientEmail: createdDirectRecipient.email,
recipientId: createdDirectRecipient.id,
@ -609,6 +609,7 @@ export const createDocumentFromDirectTemplate = async ({
},
include: {
documentData: true,
documentMeta: true,
Recipient: true,
},
});

View File

@ -26,7 +26,7 @@ import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import type { TDocumentEmailSettings } from '../../types/document-email';
import { ZFieldMetaSchema } from '../../types/field-meta';
import { ZWebhookDocumentSchema } from '../../types/webhook-payload';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import {
createDocumentAuthOptions,
@ -73,7 +73,7 @@ export type CreateDocumentFromTemplateOptions = {
typedSignatureEnabled?: boolean;
emailSettings?: TDocumentEmailSettings;
};
requestMetadata?: RequestMetadata;
requestMetadata: ApiRequestMetadata;
};
export const ZCreateDocumentFromTemplateResponseSchema = DocumentSchema.extend({
@ -95,12 +95,6 @@ export const createDocumentFromTemplate = async ({
override,
requestMetadata,
}: CreateDocumentFromTemplateOptions): Promise<TCreateDocumentFromTemplateResponse> => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const template = await prisma.template.findUnique({
where: {
id: templateId,
@ -312,8 +306,7 @@ export const createDocumentFromTemplate = async ({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id,
user,
requestMetadata,
metadata: requestMetadata,
data: {
title: document.title,
source: {

View File

@ -16,6 +16,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
export type CreateTemplateDirectLinkOptions = {
templateId: number;
userId: number;
teamId?: number;
directRecipientId?: number;
};
@ -28,25 +29,27 @@ export type TCreateTemplateDirectLinkResponse = z.infer<
export const createTemplateDirectLink = async ({
templateId,
userId,
teamId,
directRecipientId,
}: CreateTemplateDirectLinkOptions): Promise<TCreateTemplateDirectLinkResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
},
},
],
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,

View File

@ -8,29 +8,32 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
export type DeleteTemplateDirectLinkOptions = {
templateId: number;
userId: number;
teamId?: number;
};
export const deleteTemplateDirectLink = async ({
templateId,
userId,
teamId,
}: DeleteTemplateDirectLinkOptions): Promise<void> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
},
},
],
}
: {
userId,
teamId: null,
}),
},
include: {
directLink: true,

View File

@ -12,26 +12,21 @@ export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptio
return await prisma.template.delete({
where: {
id,
OR:
teamId === undefined
? [
{
userId,
teamId: null,
},
]
: [
{
teamId,
team: {
members: {
some: {
userId,
},
},
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
],
},
}
: {
userId,
teamId: null,
}),
},
});
};

View File

@ -9,6 +9,7 @@ import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/te
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
userId: number;
teamId?: number;
};
export const ZDuplicateTemplateResponseSchema = TemplateSchema;
@ -20,28 +21,25 @@ export const duplicateTemplate = async ({
userId,
teamId,
}: DuplicateTemplateOptions): Promise<TDuplicateTemplateResponse> => {
let templateWhereFilter: Prisma.TemplateWhereUniqueInput = {
id: templateId,
userId,
teamId: null,
};
if (teamId !== undefined) {
templateWhereFilter = {
id: templateId,
teamId,
team: {
members: {
some: {
userId,
},
},
},
};
}
const template = await prisma.template.findUnique({
where: templateWhereFilter,
where: {
id: templateId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,
Field: true,

View File

@ -121,12 +121,7 @@ export const findTemplates = async ({
},
Field: true,
Recipient: true,
templateMeta: {
select: {
signingOrder: true,
distributionMethod: true,
},
},
templateMeta: true,
directLink: {
select: {
token: true,

View File

@ -1,7 +1,6 @@
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import {
DocumentDataSchema,
FieldSchema,
@ -40,32 +39,25 @@ export const getTemplateById = async ({
userId,
teamId,
}: GetTemplateByIdOptions): Promise<TGetTemplateByIdResponse> => {
const whereFilter: Prisma.TemplateWhereInput = {
id,
OR:
teamId === undefined
? [
{
userId,
teamId: null,
},
]
: [
{
teamId,
team: {
members: {
some: {
userId,
},
const template = await prisma.template.findFirst({
where: {
id,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
],
};
const template = await prisma.template.findFirst({
where: whereFilter,
}
: {
userId,
teamId: null,
}),
},
include: {
directLink: true,
templateDocumentData: true,

View File

@ -10,6 +10,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
export type ToggleTemplateDirectLinkOptions = {
templateId: number;
userId: number;
teamId?: number;
enabled: boolean;
};
@ -22,25 +23,27 @@ export type TToggleTemplateDirectLinkResponse = z.infer<
export const toggleTemplateDirectLink = async ({
templateId,
userId,
teamId,
enabled,
}: ToggleTemplateDirectLinkOptions): Promise<TToggleTemplateDirectLinkResponse> => {
const template = await prisma.template.findFirst({
where: {
id: templateId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
...(teamId
? {
team: {
id: teamId,
members: {
some: {
userId,
},
},
},
},
},
],
}
: {
userId,
teamId: null,
}),
},
include: {
Recipient: true,

View File

@ -3,7 +3,6 @@
import type { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import type { DocumentVisibility, Template, TemplateMeta } from '@documenso/prisma/client';
import { TemplateSchema } from '@documenso/prisma/generated/zod';
@ -12,11 +11,11 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
export type UpdateTemplateSettingsOptions = {
export type UpdateTemplateOptions = {
userId: number;
teamId?: number;
templateId: number;
data: {
data?: {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility;
@ -27,26 +26,19 @@ export type UpdateTemplateSettingsOptions = {
type?: Template['type'];
};
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
requestMetadata?: RequestMetadata;
};
export const ZUpdateTemplateSettingsResponseSchema = TemplateSchema;
export const ZUpdateTemplateResponseSchema = TemplateSchema;
export type TUpdateTemplateSettingsResponse = z.infer<typeof ZUpdateTemplateSettingsResponseSchema>;
export type TUpdateTemplateResponse = z.infer<typeof ZUpdateTemplateResponseSchema>;
export const updateTemplateSettings = async ({
export const updateTemplate = async ({
userId,
teamId,
templateId,
meta,
data,
}: UpdateTemplateSettingsOptions): Promise<TUpdateTemplateSettingsResponse> => {
if (Object.values(data).length === 0 && Object.keys(meta ?? {}).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Missing data to update',
});
}
meta = {},
data = {},
}: UpdateTemplateOptions): Promise<TUpdateTemplateResponse> => {
const template = await prisma.template.findFirstOrThrow({
where: {
id: templateId,
@ -71,6 +63,10 @@ export const updateTemplateSettings = async ({
},
});
if (Object.values(data).length === 0 && Object.keys(meta).length === 0) {
return template;
}
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
});
@ -108,12 +104,12 @@ export const updateTemplateSettings = async ({
id: templateId,
},
data: {
title: data.title,
externalId: data.externalId,
type: data.type,
visibility: data.visibility,
publicDescription: data.publicDescription,
publicTitle: data.publicTitle,
title: data?.title,
externalId: data?.externalId,
type: data?.type,
visibility: data?.visibility,
publicDescription: data?.publicDescription,
publicTitle: data?.publicTitle,
authOptions,
templateMeta: {
upsert: {

View File

@ -8,7 +8,7 @@ import { z } from 'zod';
import { DocumentSource, FieldType } from '@documenso/prisma/client';
import { ZRecipientActionAuthTypesSchema } from './document-auth';
import { ZRecipientAccessAuthTypesSchema, ZRecipientActionAuthTypesSchema } from './document-auth';
export const ZDocumentAuditLogTypeSchema = z.enum([
// Document actions.
@ -127,11 +127,11 @@ export const ZGenericFromToSchema = z.object({
});
export const ZRecipientDiffActionAuthSchema = ZGenericFromToSchema.extend({
type: z.literal(RECIPIENT_DIFF_TYPE.ACCESS_AUTH),
type: z.literal(RECIPIENT_DIFF_TYPE.ACTION_AUTH),
});
export const ZRecipientDiffAccessAuthSchema = ZGenericFromToSchema.extend({
type: z.literal(RECIPIENT_DIFF_TYPE.ACTION_AUTH),
type: z.literal(RECIPIENT_DIFF_TYPE.ACCESS_AUTH),
});
export const ZRecipientDiffNameSchema = ZGenericFromToSchema.extend({
@ -438,6 +438,7 @@ export const ZDocumentAuditLogEventFieldUpdatedSchema = z.object({
export const ZDocumentAuditLogEventRecipientAddedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
data: ZBaseRecipientDataSchema.extend({
accessAuth: ZRecipientAccessAuthTypesSchema.optional(),
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
}),
});

View File

@ -48,7 +48,9 @@ export const ZDocumentAuthMethodsSchema = z.discriminatedUnion('type', [
* Must keep these two in sync.
*/
export const ZDocumentAccessAuthSchema = z.discriminatedUnion('type', [ZDocumentAuthAccountSchema]);
export const ZDocumentAccessAuthTypesSchema = z.enum([DocumentAuth.ACCOUNT]);
export const ZDocumentAccessAuthTypesSchema = z
.enum([DocumentAuth.ACCOUNT])
.describe('The type of authentication required for the recipient to access the document.');
/**
* The global document action auth methods.
@ -60,11 +62,11 @@ export const ZDocumentActionAuthSchema = z.discriminatedUnion('type', [
ZDocumentAuthPasskeySchema,
ZDocumentAuth2FASchema,
]);
export const ZDocumentActionAuthTypesSchema = z.enum([
DocumentAuth.ACCOUNT,
DocumentAuth.PASSKEY,
DocumentAuth.TWO_FACTOR_AUTH,
]);
export const ZDocumentActionAuthTypesSchema = z
.enum([DocumentAuth.ACCOUNT, DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH])
.describe(
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
);
/**
* The recipient access auth methods.
@ -74,7 +76,9 @@ export const ZDocumentActionAuthTypesSchema = z.enum([
export const ZRecipientAccessAuthSchema = z.discriminatedUnion('type', [
ZDocumentAuthAccountSchema,
]);
export const ZRecipientAccessAuthTypesSchema = z.enum([DocumentAuth.ACCOUNT]);
export const ZRecipientAccessAuthTypesSchema = z
.enum([DocumentAuth.ACCOUNT])
.describe('The type of authentication required for the recipient to access the document.');
/**
* The recipient action auth methods.
@ -87,12 +91,14 @@ export const ZRecipientActionAuthSchema = z.discriminatedUnion('type', [
ZDocumentAuth2FASchema,
ZDocumentAuthExplicitNoneSchema,
]);
export const ZRecipientActionAuthTypesSchema = z.enum([
DocumentAuth.ACCOUNT,
DocumentAuth.PASSKEY,
DocumentAuth.TWO_FACTOR_AUTH,
DocumentAuth.EXPLICIT_NONE,
]);
export const ZRecipientActionAuthTypesSchema = z
.enum([
DocumentAuth.ACCOUNT,
DocumentAuth.PASSKEY,
DocumentAuth.TWO_FACTOR_AUTH,
DocumentAuth.EXPLICIT_NONE,
])
.describe('The type of authentication required for the recipient to sign the document.');
export const DocumentAccessAuth = ZDocumentAccessAuthTypesSchema.Enum;
export const DocumentActionAuth = ZDocumentActionAuthTypesSchema.Enum;

View File

@ -15,13 +15,44 @@ export enum DocumentEmailEvents {
export const ZDocumentEmailSettingsSchema = z
.object({
recipientSigningRequest: z.boolean().default(true),
recipientRemoved: z.boolean().default(true),
recipientSigned: z.boolean().default(true),
documentPending: z.boolean().default(true),
documentCompleted: z.boolean().default(true),
documentDeleted: z.boolean().default(true),
ownerDocumentCompleted: z.boolean().default(true),
recipientSigningRequest: z
.boolean()
.describe(
'Whether to send an email to all recipients that the document is ready for them to sign.',
)
.default(true),
recipientRemoved: z
.boolean()
.describe(
'Whether to send an email to the recipient who was removed from a pending document.',
)
.default(true),
recipientSigned: z
.boolean()
.describe(
'Whether to send an email to the document owner when a recipient has signed the document.',
)
.default(true),
documentPending: z
.boolean()
.describe(
'Whether to send an email to the recipient who has just signed the document indicating that there are still other recipients who need to sign the document. This will only be sent if the document is still pending after the recipient has signed.',
)
.default(true),
documentCompleted: z
.boolean()
.describe('Whether to send an email to all recipients when the document is complete.')
.default(true),
documentDeleted: z
.boolean()
.describe(
'Whether to send an email to all recipients if a pending document has been deleted.',
)
.default(true),
ownerDocumentCompleted: z
.boolean()
.describe('Whether to send an email to the document owner when the document is complete.')
.default(true),
})
.strip()
.catch(() => ({

View File

@ -8,9 +8,9 @@ import { z } from 'zod';
* Keep this and `ZUrlSearchParamsSchema` in sync.
*/
export const ZFindSearchParamsSchema = z.object({
query: z.string().optional(),
page: z.coerce.number().min(1).optional(),
perPage: z.coerce.number().min(1).optional(),
query: z.string().describe('The search query.').optional(),
page: z.coerce.number().min(1).describe('The pagination page number, starts at 1.').optional(),
perPage: z.coerce.number().min(1).describe('The number of items per page.').max(100).optional(),
});
/**
@ -31,16 +31,17 @@ export const ZUrlSearchParamsSchema = z.object({
perPage: z.coerce
.number()
.min(1)
.max(100)
.optional()
.catch(() => undefined),
});
export const ZFindResultResponse = z.object({
data: z.union([z.array(z.unknown()), z.unknown()]),
count: z.number(),
currentPage: z.number(),
perPage: z.number(),
totalPages: z.number(),
data: z.union([z.array(z.unknown()), z.unknown()]).describe('The results from the search.'),
count: z.number().describe('The total number of items.'),
currentPage: z.number().describe('The current page number, starts at 1.'),
perPage: z.number().describe('The number of items per page.'),
totalPages: z.number().describe('The total number of pages.'),
});
// Can't infer generics from Zod.

View File

@ -12,6 +12,36 @@ export const ZRequestMetadataSchema = z.object({
export type RequestMetadata = z.infer<typeof ZRequestMetadataSchema>;
export type ApiRequestMetadata = {
/**
* The general metadata of the request.
*/
requestMetadata: RequestMetadata;
/**
* The source of the request.
*/
source: 'apiV1' | 'apiV2' | 'app';
/**
* The method of authentication used to access the API.
*
* If the request is not authenticated, the value will be `null`.
*/
auth: 'api' | 'session' | null;
/**
* The user that is performing the action.
*
* If a team API key is used, the user will classified as the team.
*/
auditUser?: {
id: number | null;
email: string | null;
name: string | null;
};
};
export const extractNextApiRequestMetadata = (req: NextApiRequest): RequestMetadata => {
const parsedIp = ZIpSchema.safeParse(req.headers['x-forwarded-for'] || req.socket.remoteAddress);

View File

@ -19,14 +19,15 @@ import {
ZDocumentAuditLogSchema,
} from '../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../types/document-auth';
import type { RequestMetadata } from '../universal/extract-request-metadata';
import type { ApiRequestMetadata, RequestMetadata } from '../universal/extract-request-metadata';
type CreateDocumentAuditLogDataOptions<T = TDocumentAuditLog['type']> = {
documentId: number;
type: T;
data: Extract<TDocumentAuditLog, { type: T }>['data'];
user: { email?: string; id?: number | null; name?: string | null } | null;
user?: { email?: string | null; id?: number | null; name?: string | null } | null;
requestMetadata?: RequestMetadata;
metadata?: ApiRequestMetadata;
};
export type CreateDocumentAuditLogDataResponse = Pick<
@ -42,16 +43,31 @@ export const createDocumentAuditLogData = <T extends TDocumentAuditLog['type']>(
data,
user,
requestMetadata,
metadata,
}: CreateDocumentAuditLogDataOptions<T>): CreateDocumentAuditLogDataResponse => {
let userId: number | null = metadata?.auditUser?.id || null;
let email: string | null = metadata?.auditUser?.email || null;
let name: string | null = metadata?.auditUser?.name || null;
// Prioritize explicit user parameter over metadata audit user.
if (user) {
userId = user.id || null;
email = user.email || null;
name = user.name || null;
}
const ipAddress = metadata?.requestMetadata.ipAddress ?? requestMetadata?.ipAddress ?? null;
const userAgent = metadata?.requestMetadata.userAgent ?? requestMetadata?.userAgent ?? null;
return {
type,
data,
documentId,
userId: user?.id ?? null,
email: user?.email ?? null,
name: user?.name ?? null,
userAgent: requestMetadata?.userAgent ?? null,
ipAddress: requestMetadata?.ipAddress ?? null,
userId,
email,
name,
userAgent,
ipAddress,
};
};

View File

@ -8,8 +8,17 @@ export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}
* Whether a recipient can be modified by the document owner.
*/
export const canRecipientBeModified = (recipient: Recipient, fields: Field[]) => {
if (!recipient) {
return false;
}
// CCers can always be modified (unless document is completed).
if (recipient.role === RecipientRole.CC) {
return true;
}
// Deny if the recipient has already signed the document.
if (!recipient || recipient.signingStatus === SigningStatus.SIGNED) {
if (recipient.signingStatus === SigningStatus.SIGNED) {
return false;
}

View File

@ -35,9 +35,10 @@ export const trpc = createTRPCReact<AppRouter>({
export interface TrpcProviderProps {
children: React.ReactNode;
headers?: Record<string, string>;
}
export function TrpcProvider({ children }: TrpcProviderProps) {
export function TrpcProvider({ children, headers }: TrpcProviderProps) {
let queryClientConfig: QueryClientConfig | undefined;
const isDevelopingOffline =
@ -63,15 +64,16 @@ export function TrpcProvider({ children }: TrpcProviderProps) {
const [trpcClient] = useState(() =>
trpc.createClient({
transformer: SuperJSON,
links: [
splitLink({
condition: (op) => op.context.skipBatch === true,
true: httpLink({
url: `${getBaseUrl()}/api/trpc`,
headers,
}),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers,
}),
}),
],

View File

@ -17,7 +17,6 @@ import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
import { createUser } from '@documenso/lib/server-only/user/create-user';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
@ -89,7 +88,7 @@ export const authRouter = router({
userId: ctx.user.id,
verificationResponse,
passkeyName: input.passkeyName,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),
@ -132,7 +131,7 @@ export const authRouter = router({
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),
@ -158,7 +157,7 @@ export const authRouter = router({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),
});

View File

@ -1,15 +1,37 @@
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { z } from 'zod';
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
import type { CreateNextContextOptions } from './adapters/next';
type CreateTrpcContext = CreateNextContextOptions & {
requestSource: 'apiV1' | 'apiV2' | 'app';
};
export const createTrpcContext = async ({ req, res, requestSource }: CreateTrpcContext) => {
const { session, user } = await getServerSession({ req, res });
const metadata: ApiRequestMetadata = {
requestMetadata: extractNextApiRequestMetadata(req),
source: requestSource,
auth: null,
};
const teamId = z.coerce
.number()
.optional()
.catch(() => undefined)
.parse(req.headers['x-team-id']);
if (!session) {
return {
session: null,
user: null,
teamId,
req,
metadata,
};
}
@ -17,14 +39,18 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions)
return {
session: null,
user: null,
teamId,
req,
metadata,
};
}
return {
session,
user,
teamId,
req,
metadata,
};
};

View File

@ -39,12 +39,10 @@ import {
sendDocument,
} from '@documenso/lib/server-only/document/send-document';
import {
ZUpdateDocumentSettingsResponseSchema,
updateDocumentSettings,
} from '@documenso/lib/server-only/document/update-document-settings';
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
ZUpdateDocumentResponseSchema,
updateDocument,
} from '@documenso/lib/server-only/document/update-document';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { DocumentStatus } from '@documenso/prisma/client';
import { authenticatedProcedure, procedure, router } from '../trpc';
@ -64,9 +62,8 @@ import {
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
ZSetPasswordForDocumentMutationSchema,
ZSetSettingsForDocumentMutationSchema,
ZSetSigningOrderForDocumentMutationSchema,
ZSetTitleForDocumentMutationSchema,
ZUpdateDocumentRequestSchema,
ZUpdateTypedSignatureSettingsMutationSchema,
} from './schema';
@ -77,9 +74,13 @@ export const documentRouter = router({
getDocumentById: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
return await getDocumentById({
...input,
userId: ctx.user.id,
teamId,
documentId,
});
}),
@ -104,28 +105,19 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'GET',
path: '/document/find',
path: '/document',
summary: 'Find documents',
description: 'Find documents based on a search criteria',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZFindDocumentsQuerySchema)
.output(ZFindDocumentsResponseSchema)
.query(async ({ input, ctx }) => {
const { user } = ctx;
const { user, teamId } = ctx;
const {
query,
teamId,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
} = input;
const { query, templateId, page, perPage, orderByDirection, orderByColumn, source, status } =
input;
const documents = await findDocuments({
userId: user.id,
@ -154,34 +146,41 @@ export const documentRouter = router({
path: '/document/{documentId}',
summary: 'Get document',
description: 'Returns a document given an ID',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZGetDocumentWithDetailsByIdQuerySchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
return await getDocumentWithDetailsById({
...input,
userId: ctx.user.id,
userId: user.id,
teamId,
documentId,
});
}),
/**
* @public
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/create',
summary: 'Create document',
tags: ['Documents'],
},
})
// .meta({
// openapi: {
// method: 'POST',
// path: '/document/create',
// summary: 'Create document',
// tags: ['Document'],
// },
// })
.input(ZCreateDocumentMutationSchema)
.output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { title, documentDataId, teamId, timezone } = input;
const { teamId } = ctx;
const { title, documentDataId, timezone } = input;
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
@ -199,7 +198,7 @@ export const documentRouter = router({
documentDataId,
normalizePdf: true,
timezone,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@ -212,38 +211,43 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}',
path: '/document/update',
summary: 'Update document',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZSetSettingsForDocumentMutationSchema)
.output(ZUpdateDocumentSettingsResponseSchema)
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, data, meta } = input;
const { teamId } = ctx;
const { documentId, data, meta = {} } = input;
const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
if (meta.timezone || meta.dateFormat || meta.redirectUrl) {
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
documentId,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
language: meta.language,
userId: ctx.user.id,
requestMetadata,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings,
requestMetadata: ctx.metadata,
});
}
return await updateDocumentSettings({
return await updateDocument({
userId,
teamId,
documentId,
data,
requestMetadata,
requestMetadata: ctx.metadata,
});
}),
@ -253,16 +257,17 @@ export const documentRouter = router({
deleteDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/delete',
method: 'DELETE',
path: '/document/{documentId}',
summary: 'Delete document',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZDeleteDocumentMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const { teamId } = ctx;
const { documentId } = input;
const userId = ctx.user.id;
@ -270,7 +275,7 @@ export const documentRouter = router({
id: documentId,
userId,
teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@ -281,10 +286,10 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/move',
path: '/document/move',
summary: 'Move document',
description: 'Move a document to a team',
tags: ['Documents'],
description: 'Move a document from your personal account to a team',
tags: ['Document'],
},
})
.input(ZMoveDocumentToTeamSchema)
@ -297,27 +302,7 @@ export const documentRouter = router({
documentId,
teamId,
userId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
/**
* @private
*/
// Should probably use `updateDocument`
setTitleForDocument: authenticatedProcedure
.input(ZSetTitleForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, title } = input;
const userId = ctx.user.id;
return await updateTitle({
title,
userId,
teamId,
documentId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@ -327,6 +312,7 @@ export const documentRouter = router({
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, password } = input;
const key = DOCUMENSO_ENCRYPTION_KEY;
@ -341,10 +327,11 @@ export const documentRouter = router({
});
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
password: securePassword,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@ -354,23 +341,28 @@ export const documentRouter = router({
setSigningOrderForDocument: authenticatedProcedure
.input(ZSetSigningOrderForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, signingOrder } = input;
return await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
signingOrder,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
/**
* @deprecated Remove after deployment.
*
* @private
*/
updateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, typedSignatureEnabled } = input;
const { teamId } = ctx;
const { documentId, typedSignatureEnabled } = input;
const document = await getDocumentById({
documentId,
@ -386,10 +378,11 @@ export const documentRouter = router({
}
return await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
typedSignatureEnabled,
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@ -403,27 +396,22 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/distribute',
path: '/document/distribute',
summary: 'Distribute document',
description: 'Send the document out to recipients based on your distribution method',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZSendDocumentMutationSchema)
.output(ZSendDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, meta } = input;
const { teamId } = ctx;
const { documentId, meta = {} } = input;
if (
meta.message ||
meta.subject ||
meta.timezone ||
meta.dateFormat ||
meta.redirectUrl ||
meta.distributionMethod ||
meta.emailSettings
) {
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
@ -431,9 +419,9 @@ export const documentRouter = router({
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
userId: ctx.user.id,
emailSettings: meta.emailSettings,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
language: meta.language,
requestMetadata: ctx.metadata,
});
}
@ -441,31 +429,38 @@ export const documentRouter = router({
userId: ctx.user.id,
documentId,
teamId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*
* Todo: Refactor to redistributeDocument.
*/
resendDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/resend',
summary: 'Resend document',
path: '/document/redistribute',
summary: 'Redistribute document',
description:
'Resend the document to recipients who have not signed. Will use the distribution method set in the document.',
tags: ['Documents'],
'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
tags: ['Document'],
},
})
.input(ZResendDocumentMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await resendDocument({
userId: ctx.user.id,
...input,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
}),
@ -476,17 +471,21 @@ export const documentRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/duplicate',
path: '/document/duplicate',
summary: 'Duplicate document',
tags: ['Documents'],
tags: ['Document'],
},
})
.input(ZDuplicateDocumentMutationSchema)
.output(ZDuplicateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
return await duplicateDocument({
userId: ctx.user.id,
...input,
userId: user.id,
teamId,
documentId,
});
}),
@ -512,6 +511,8 @@ export const documentRouter = router({
findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const {
page,
perPage,
@ -523,13 +524,14 @@ export const documentRouter = router({
} = input;
return await findDocumentAuditLogs({
userId: ctx.user.id,
teamId,
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
userId: ctx.user.id,
});
}),
@ -539,7 +541,8 @@ export const documentRouter = router({
downloadAuditLogs: authenticatedProcedure
.input(ZDownloadAuditLogsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const { teamId } = ctx;
const { documentId } = input;
const document = await getDocumentById({
documentId,
@ -570,7 +573,8 @@ export const documentRouter = router({
downloadCertificate: authenticatedProcedure
.input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const { teamId } = ctx;
const { documentId } = input;
const document = await getDocumentById({
documentId,

View File

@ -15,16 +15,59 @@ import {
DocumentStatus,
DocumentVisibility,
FieldType,
RecipientRole,
} from '@documenso/prisma/client';
// Todo: Refactor all to ZDocumentMeta---
export const ZDocumentMetaTimezoneSchema = z
.string()
.describe('The timezone to use for date fields and signing the document.');
export const ZDocumentMetaDateFormatSchema = z
.string()
.describe('The date format to use for date fields and signing the document.');
export const ZDocumentMetaRedirectUrlSchema = z
.string()
.describe('The URL to which the recipient should be redirected after signing the document.')
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
});
export const ZDocumentMetaLanguageSchema = z
.enum(SUPPORTED_LANGUAGE_CODES)
.describe('The language to use for email communications with recipients.');
export const ZDocumentMetaSubjectSchema = z
.string()
.describe('The subject of the email that will be sent to the recipients.');
export const ZDocumentMetaMessageSchema = z
.string()
.describe('The message of the email that will be sent to the recipients.');
export const ZDocumentMetaDistributionMethodSchema = z
.nativeEnum(DocumentDistributionMethod)
.describe('The distribution method to use when sending the document to the recipients.');
export const ZDocumentMetaTypedSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow typed signatures.');
export const ZFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number().min(1).optional(),
templateId: z.number().min(1).optional(),
source: z.nativeEnum(DocumentSource).optional(),
status: z.nativeEnum(DocumentStatus).optional(),
templateId: z
.number()
.describe('Filter documents by the template ID used to create it.')
.optional(),
source: z
.nativeEnum(DocumentSource)
.describe('Filter documents by how it was created.')
.optional(),
status: z
.nativeEnum(DocumentStatus)
.describe('Filter documents by the current status')
.optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
});
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
@ -36,13 +79,11 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
});
export const ZGetDocumentByIdQuerySchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export const ZDuplicateDocumentMutationSchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TGetDocumentByIdQuerySchema = z.infer<typeof ZGetDocumentByIdQuerySchema>;
@ -54,8 +95,7 @@ export const ZGetDocumentByTokenQuerySchema = z.object({
export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQuerySchema>;
export const ZGetDocumentWithDetailsByIdQuerySchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TGetDocumentWithDetailsByIdQuerySchema = z.infer<
@ -65,64 +105,41 @@ export type TGetDocumentWithDetailsByIdQuerySchema = z.infer<
export const ZCreateDocumentMutationSchema = z.object({
title: z.string().min(1),
documentDataId: z.string().min(1),
teamId: z.number().optional(),
timezone: z.string().optional(),
});
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
export const ZSetSettingsForDocumentMutationSchema = z.object({
export const ZUpdateDocumentRequestSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
data: z.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
}),
meta: z.object({
timezone: z.string(),
dateFormat: z.string(),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
}),
data: z
.object({
title: z.string().describe('The title of the document.').min(1).optional(),
externalId: z.string().nullish().describe('The external ID of the document.'),
visibility: z
.nativeEnum(DocumentVisibility)
.describe('The visibility of the document.')
.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export type TSetGeneralSettingsForDocumentMutationSchema = z.infer<
typeof ZSetSettingsForDocumentMutationSchema
>;
export const ZSetTitleForDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
title: z.string().min(1),
});
export type TSetTitleForDocumentMutationSchema = z.infer<typeof ZSetTitleForDocumentMutationSchema>;
export const ZSetRecipientsForDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
recipients: z.array(
z.object({
id: z.number().nullish(),
email: z.string().min(1).email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
}),
),
});
export type TSetRecipientsForDocumentMutationSchema = z.infer<
typeof ZSetRecipientsForDocumentMutationSchema
>;
export type TUpdateDocumentRequestSchema = z.infer<typeof ZUpdateDocumentRequestSchema>;
export const ZSetFieldsForDocumentMutationSchema = z.object({
documentId: z.number(),
@ -145,23 +162,19 @@ export type TSetFieldsForDocumentMutationSchema = z.infer<
>;
export const ZSendDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
meta: z.object({
subject: z.string(),
message: z.string(),
timezone: z.string().optional(),
dateFormat: z.string().optional(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
}),
documentId: z.number().describe('The ID of the document to send.'),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZSetPasswordForDocumentMutationSchema = z.object({
@ -184,7 +197,6 @@ export type TSetSigningOrderForDocumentMutationSchema = z.infer<
export const ZUpdateTypedSignatureSettingsMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
typedSignatureEnabled: z.boolean(),
});
@ -194,15 +206,16 @@ export type TUpdateTypedSignatureSettingsMutationSchema = z.infer<
export const ZResendDocumentMutationSchema = z.object({
documentId: z.number(),
recipients: z.array(z.number()).min(1),
teamId: z.number().min(1).optional(),
recipients: z
.array(z.number())
.min(1)
.describe('The IDs of the recipients to redistribute the document to.'),
});
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
export const ZDeleteDocumentMutationSchema = z.object({
documentId: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TDeleteDocumentMutationSchema = z.infer<typeof ZDeleteDocumentMutationSchema>;
@ -213,15 +226,13 @@ export const ZSearchDocumentsMutationSchema = z.object({
export const ZDownloadAuditLogsMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
});
export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
});
export const ZMoveDocumentToTeamSchema = z.object({
documentId: z.number(),
teamId: z.number(),
documentId: z.number().describe('The ID of the document to move to a team.'),
teamId: z.number().describe('The ID of the team to move the document to.'),
});

View File

@ -1,3 +1,15 @@
import { z } from 'zod';
import {
ZCreateDocumentFieldsResponseSchema,
createDocumentFields,
} from '@documenso/lib/server-only/field/create-document-fields';
import {
ZCreateTemplateFieldsResponseSchema,
createTemplateFields,
} from '@documenso/lib/server-only/field/create-template-fields';
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
import { deleteTemplateField } from '@documenso/lib/server-only/field/delete-template-field';
import {
ZGetFieldByIdResponseSchema,
getFieldById,
@ -12,15 +24,37 @@ import {
setFieldsForTemplate,
} from '@documenso/lib/server-only/field/set-fields-for-template';
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
import {
ZUpdateDocumentFieldsResponseSchema,
updateDocumentFields,
} from '@documenso/lib/server-only/field/update-document-fields';
import {
ZUpdateTemplateFieldsResponseSchema,
updateTemplateFields,
} from '@documenso/lib/server-only/field/update-template-fields';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZAddFieldsMutationSchema,
ZAddTemplateFieldsMutationSchema,
ZCreateDocumentFieldRequestSchema,
ZCreateDocumentFieldResponseSchema,
ZCreateDocumentFieldsRequestSchema,
ZCreateTemplateFieldRequestSchema,
ZCreateTemplateFieldResponseSchema,
ZCreateTemplateFieldsRequestSchema,
ZDeleteDocumentFieldRequestSchema,
ZDeleteTemplateFieldRequestSchema,
ZGetFieldQuerySchema,
ZRemovedSignedFieldWithTokenMutationSchema,
ZSignFieldWithTokenMutationSchema,
ZUpdateDocumentFieldRequestSchema,
ZUpdateDocumentFieldResponseSchema,
ZUpdateDocumentFieldsRequestSchema,
ZUpdateTemplateFieldRequestSchema,
ZUpdateTemplateFieldResponseSchema,
ZUpdateTemplateFieldsRequestSchema,
} from './schema';
export const fieldRouter = router({
@ -35,13 +69,14 @@ export const fieldRouter = router({
summary: 'Get field',
description:
'Returns a single field. If you want to retrieve all the fields for a document or template, use the "Get Document" or "Get Template" request.',
tags: ['Fields'],
tags: ['Document Fields', 'Template Fields'],
},
})
.input(ZGetFieldQuerySchema)
.output(ZGetFieldByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { fieldId, teamId } = input;
const { teamId } = ctx;
const { fieldId } = input;
return await getFieldById({
userId: ctx.user.id,
@ -53,23 +88,167 @@ export const fieldRouter = router({
/**
* @public
*/
addFields: authenticatedProcedure
createDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/field',
summary: 'Set document fields',
tags: ['Fields'],
path: '/document/field/create',
summary: 'Create document field',
description: 'Create a single field for a document.',
tags: ['Document Fields'],
},
})
.input(ZCreateDocumentFieldRequestSchema)
.output(ZCreateDocumentFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, field } = input;
const createdFields = await createDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields: [field],
requestMetadata: ctx.metadata,
});
return createdFields.fields[0];
}),
/**
* @public
*/
createDocumentFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/create-many',
summary: 'Create document fields',
description: 'Create multiple fields for a document.',
tags: ['Document Fields'],
},
})
.input(ZCreateDocumentFieldsRequestSchema)
.output(ZCreateDocumentFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, fields } = input;
return await createDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
updateDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/update',
summary: 'Update document field',
description: 'Update a single field for a document.',
tags: ['Document Fields'],
},
})
.input(ZUpdateDocumentFieldRequestSchema)
.output(ZUpdateDocumentFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, field } = input;
const updatedFields = await updateDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields: [field],
requestMetadata: ctx.metadata,
});
return updatedFields.fields[0];
}),
/**
* @public
*/
updateDocumentFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/update-many',
summary: 'Update document fields',
description: 'Update multiple fields for a document.',
tags: ['Document Fields'],
},
})
.input(ZUpdateDocumentFieldsRequestSchema)
.output(ZUpdateDocumentFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, fields } = input;
return await updateDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
deleteDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/document/field/{fieldId}',
summary: 'Delete document field',
tags: ['Document Fields'],
},
})
.input(ZDeleteDocumentFieldRequestSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { fieldId } = input;
await deleteDocumentField({
userId: ctx.user.id,
teamId,
fieldId,
requestMetadata: ctx.metadata,
});
}),
/**
* @private
*/
addFields: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/document/{documentId}/field',
// summary: 'Set document fields',
// tags: ['Document Fields'],
// },
// })
.input(ZAddFieldsMutationSchema)
.output(ZSetFieldsForDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, fields } = input;
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
teamId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
@ -81,30 +260,169 @@ export const fieldRouter = router({
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
addTemplateFields: authenticatedProcedure
createTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/field',
summary: 'Set template fields',
tags: ['Fields'],
path: '/template/field/create',
summary: 'Create template field',
description: 'Create a single field for a template.',
tags: ['Template Fields'],
},
})
.input(ZCreateTemplateFieldRequestSchema)
.output(ZCreateTemplateFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, field } = input;
const createdFields = await createTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields: [field],
});
return createdFields.fields[0];
}),
/**
* @public
*/
createTemplateFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/create-many',
summary: 'Create template fields',
description: 'Create multiple fields for a template.',
tags: ['Template Fields'],
},
})
.input(ZCreateTemplateFieldsRequestSchema)
.output(ZCreateTemplateFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, fields } = input;
return await createTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields,
});
}),
/**
* @public
*/
updateTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/update',
summary: 'Update template field',
description: 'Update a single field for a template.',
tags: ['Template Fields'],
},
})
.input(ZUpdateTemplateFieldRequestSchema)
.output(ZUpdateTemplateFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, field } = input;
const updatedFields = await updateTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields: [field],
});
return updatedFields.fields[0];
}),
/**
* @public
*/
updateTemplateFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/update-many',
summary: 'Update template fields',
description: 'Update multiple fields for a template.',
tags: ['Template Fields'],
},
})
.input(ZUpdateTemplateFieldsRequestSchema)
.output(ZUpdateTemplateFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, fields } = input;
return await updateTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields,
});
}),
/**
* @public
*/
deleteTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/template/field/{fieldId}',
summary: 'Delete template field',
tags: ['Template Fields'],
},
})
.input(ZDeleteTemplateFieldRequestSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { fieldId } = input;
await deleteTemplateField({
userId: ctx.user.id,
teamId,
fieldId,
});
}),
/**
* @private
*/
addTemplateFields: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/{templateId}/field',
// summary: 'Set template fields',
// tags: ['Template Fields'],
// },
// })
.input(ZAddTemplateFieldsMutationSchema)
.output(ZSetFieldsForTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, fields } = input;
return await setFieldsForTemplate({
userId: ctx.user.id,
templateId,
userId: ctx.user.id,
teamId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,

View File

@ -3,6 +3,82 @@ import { z } from 'zod';
import { ZRecipientActionAuthSchema } from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import { FieldSchema } from '@documenso/prisma/generated/zod';
const ZCreateFieldSchema = z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for.'),
type: FieldSchema.shape.type.describe('The type of the field to create.'),
pageNumber: z.number().describe('The page number the field will be on.'),
pageX: z.number().describe('The X coordinate of where the field will be placed.'),
pageY: z.number().describe('The Y coordinate of where the field will be placed.'),
width: z.number().describe('The width of the field.'),
height: z.number().describe('The height of the field.'),
fieldMeta: ZFieldMetaSchema.optional(),
});
const ZUpdateFieldSchema = z.object({
id: z.number().describe('The ID of the field to update.'),
type: FieldSchema.shape.type.optional().describe('The type of the field to update.'),
pageNumber: z.number().optional().describe('The page number the field will be on.'),
pageX: z.number().optional().describe('The X coordinate of where the field will be placed.'),
pageY: z.number().optional().describe('The Y coordinate of where the field will be placed.'),
width: z.number().optional().describe('The width of the field.'),
height: z.number().optional().describe('The height of the field.'),
fieldMeta: ZFieldMetaSchema.optional(),
});
export const ZCreateDocumentFieldRequestSchema = z.object({
documentId: z.number().min(1),
field: ZCreateFieldSchema,
});
export const ZCreateDocumentFieldsRequestSchema = z.object({
documentId: z.number().min(1),
fields: ZCreateFieldSchema.array(),
});
export const ZUpdateDocumentFieldRequestSchema = z.object({
documentId: z.number().min(1),
field: ZUpdateFieldSchema,
});
export const ZUpdateDocumentFieldsRequestSchema = z.object({
documentId: z.number().min(1),
fields: ZUpdateFieldSchema.array(),
});
export const ZDeleteDocumentFieldRequestSchema = z.object({
fieldId: z.number().min(1),
});
export const ZCreateTemplateFieldRequestSchema = z.object({
templateId: z.number().min(1),
field: ZCreateFieldSchema,
});
export const ZCreateDocumentFieldResponseSchema = FieldSchema;
export const ZUpdateTemplateFieldResponseSchema = FieldSchema;
export const ZUpdateDocumentFieldResponseSchema = FieldSchema;
export const ZCreateTemplateFieldResponseSchema = FieldSchema;
export const ZCreateTemplateFieldsRequestSchema = z.object({
templateId: z.number().min(1),
fields: ZCreateFieldSchema.array(),
});
export const ZUpdateTemplateFieldRequestSchema = z.object({
templateId: z.number().min(1),
field: ZUpdateFieldSchema,
});
export const ZUpdateTemplateFieldsRequestSchema = z.object({
templateId: z.number().min(1),
fields: ZUpdateFieldSchema.array(),
});
export const ZDeleteTemplateFieldRequestSchema = z.object({
fieldId: z.number().min(1),
});
export const ZAddFieldsMutationSchema = z.object({
documentId: z.number(),
@ -65,7 +141,6 @@ export type TRemovedSignedFieldWithTokenMutationSchema = z.infer<
export const ZGetFieldQuerySchema = z.object({
fieldId: z.number(),
teamId: z.number().optional(),
});
export type TGetFieldQuerySchema = z.infer<typeof ZGetFieldQuerySchema>;

View File

@ -5,8 +5,8 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { appRouter } from './router';
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Do not use.',
title: 'Documenso v2 beta API',
description: 'Subject to breaking changes until v2 is fully released.',
version: '0.0.0',
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/beta`,
// docsUrl: '', // Todo
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta`,
});

View File

@ -52,7 +52,7 @@ export const profileRouter = router({
userId: ctx.user.id,
name,
signature,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),
@ -96,7 +96,7 @@ export const profileRouter = router({
userId: ctx.user.id,
password,
currentPassword,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),
@ -146,7 +146,7 @@ export const profileRouter = router({
userId: ctx.user.id,
teamId,
bytes,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
});

View File

@ -1,26 +1,57 @@
import { z } from 'zod';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token';
import {
ZCreateDocumentRecipientsResponseSchema,
createDocumentRecipients,
} from '@documenso/lib/server-only/recipient/create-document-recipients';
import {
ZCreateTemplateRecipientsResponseSchema,
createTemplateRecipients,
} from '@documenso/lib/server-only/recipient/create-template-recipients';
import { deleteDocumentRecipient } from '@documenso/lib/server-only/recipient/delete-document-recipient';
import { deleteTemplateRecipient } from '@documenso/lib/server-only/recipient/delete-template-recipient';
import {
ZGetRecipientByIdResponseSchema,
getRecipientById,
} from '@documenso/lib/server-only/recipient/get-recipient-by-id';
import {
ZSetRecipientsForDocumentResponseSchema,
setRecipientsForDocument,
} from '@documenso/lib/server-only/recipient/set-recipients-for-document';
ZSetDocumentRecipientsResponseSchema,
setDocumentRecipients,
} from '@documenso/lib/server-only/recipient/set-document-recipients';
import {
ZSetRecipientsForTemplateResponseSchema,
setRecipientsForTemplate,
} from '@documenso/lib/server-only/recipient/set-recipients-for-template';
ZSetTemplateRecipientsResponseSchema,
setTemplateRecipients,
} from '@documenso/lib/server-only/recipient/set-template-recipients';
import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients';
import { updateTemplateRecipients } from '@documenso/lib/server-only/recipient/update-template-recipients';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZAddSignersMutationSchema,
ZAddTemplateSignersMutationSchema,
ZCompleteDocumentWithTokenMutationSchema,
ZCreateDocumentRecipientRequestSchema,
ZCreateDocumentRecipientResponseSchema,
ZCreateDocumentRecipientsRequestSchema,
ZCreateTemplateRecipientRequestSchema,
ZCreateTemplateRecipientResponseSchema,
ZCreateTemplateRecipientsRequestSchema,
ZDeleteDocumentRecipientRequestSchema,
ZDeleteTemplateRecipientRequestSchema,
ZGetRecipientQuerySchema,
ZRejectDocumentWithTokenMutationSchema,
ZSetDocumentRecipientsRequestSchema,
ZSetTemplateRecipientsRequestSchema,
ZUpdateDocumentRecipientRequestSchema,
ZUpdateDocumentRecipientResponseSchema,
ZUpdateDocumentRecipientsRequestSchema,
ZUpdateDocumentRecipientsResponseSchema,
ZUpdateTemplateRecipientRequestSchema,
ZUpdateTemplateRecipientResponseSchema,
ZUpdateTemplateRecipientsRequestSchema,
ZUpdateTemplateRecipientsResponseSchema,
} from './schema';
export const recipientRouter = router({
@ -35,13 +66,14 @@ export const recipientRouter = router({
summary: 'Get recipient',
description:
'Returns a single recipient. If you want to retrieve all the recipients for a document or template, use the "Get Document" or "Get Template" request.',
tags: ['Recipients'],
tags: ['Document Recipients', 'Template Recipients'],
},
})
.input(ZGetRecipientQuerySchema)
.output(ZGetRecipientByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { recipientId, teamId } = input;
const { teamId } = ctx;
const { recipientId } = input;
return await getRecipientById({
userId: ctx.user.id,
@ -53,64 +85,349 @@ export const recipientRouter = router({
/**
* @public
*/
addSigners: authenticatedProcedure
createDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/{documentId}/recipient/set',
summary: 'Set document recipients',
tags: ['Recipients'],
path: '/document/recipient/create',
summary: 'Create document recipient',
description: 'Create a single recipient for a document.',
tags: ['Document Recipients'],
},
})
.input(ZAddSignersMutationSchema)
.output(ZSetRecipientsForDocumentResponseSchema)
.input(ZCreateDocumentRecipientRequestSchema)
.output(ZCreateDocumentRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId, signers } = input;
const { teamId } = ctx;
const { documentId, recipient } = input;
return await setRecipientsForDocument({
const createdRecipients = await createDocumentRecipients({
userId: ctx.user.id,
documentId,
teamId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
documentId,
recipients: [recipient],
requestMetadata: ctx.metadata,
});
return createdRecipients.recipients[0];
}),
/**
* @public
*/
createDocumentRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/create-many',
summary: 'Create document recipients',
description: 'Create multiple recipients for a document.',
tags: ['Document Recipients'],
},
})
.input(ZCreateDocumentRecipientsRequestSchema)
.output(ZCreateDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await createDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
addTemplateSigners: authenticatedProcedure
updateDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/recipient/set',
summary: 'Set template recipients',
tags: ['Recipients'],
path: '/document/recipient/update',
summary: 'Update document recipient',
description: 'Update a single recipient for a document.',
tags: ['Document Recipients'],
},
})
.input(ZAddTemplateSignersMutationSchema)
.output(ZSetRecipientsForTemplateResponseSchema)
.input(ZUpdateDocumentRecipientRequestSchema)
.output(ZUpdateDocumentRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, signers, teamId } = input;
const { teamId } = ctx;
const { documentId, recipient } = input;
return await setRecipientsForTemplate({
const updatedRecipients = await updateDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients: [recipient],
requestMetadata: ctx.metadata,
});
return updatedRecipients.recipients[0];
}),
/**
* @public
*/
updateDocumentRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/update-many',
summary: 'Update document recipients',
description: 'Update multiple recipients for a document.',
tags: ['Document Recipients'],
},
})
.input(ZUpdateDocumentRecipientsRequestSchema)
.output(ZUpdateDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await updateDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
deleteDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/document/recipient/{recipientId}',
summary: 'Delete document recipient',
tags: ['Document Recipients'],
},
})
.input(ZDeleteDocumentRecipientRequestSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { recipientId } = input;
await deleteDocumentRecipient({
userId: ctx.user.id,
teamId,
recipientId,
requestMetadata: ctx.metadata,
});
}),
/**
* @private
*/
setDocumentRecipients: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/document/recipient/set',
// summary: 'Set document recipients',
// description:
// 'This will replace all recipients attached to the document. If the array contains existing recipients, they will be updated and the original fields will be retained.',
// tags: ['Document Recipients'],
// },
// })
.input(ZSetDocumentRecipientsRequestSchema)
.output(ZSetDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await setDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients: recipients.map((recipient) => ({
id: recipient.nativeId,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
actionAuth: recipient.actionAuth,
})),
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
createTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/create',
summary: 'Create template recipient',
description: 'Create a single recipient for a template.',
tags: ['Template Recipients'],
},
})
.input(ZCreateTemplateRecipientRequestSchema)
.output(ZCreateTemplateRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipient } = input;
const createdRecipients = await createTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
recipients: [recipient],
});
return createdRecipients.recipients[0];
}),
/**
* @public
*/
createTemplateRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/create-many',
summary: 'Create template recipients',
description: 'Create multiple recipients for a template.',
tags: ['Template Recipients'],
},
})
.input(ZCreateTemplateRecipientsRequestSchema)
.output(ZCreateTemplateRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipients } = input;
return await createTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients,
});
}),
/**
* @public
*/
updateTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/update',
summary: 'Update template recipient',
description: 'Update a single recipient for a template.',
tags: ['Template Recipients'],
},
})
.input(ZUpdateTemplateRecipientRequestSchema)
.output(ZUpdateTemplateRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipient } = input;
const updatedRecipients = await updateTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients: [recipient],
});
return updatedRecipients.recipients[0];
}),
/**
* @public
*/
updateTemplateRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/update-many',
summary: 'Update template recipients',
description: 'Update multiple recipients for a template.',
tags: ['Template Recipients'],
},
})
.input(ZUpdateTemplateRecipientsRequestSchema)
.output(ZUpdateTemplateRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipients } = input;
return await updateTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients,
});
}),
/**
* @public
*/
deleteTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/template/recipient/{recipientId}',
summary: 'Delete template recipient',
tags: ['Template Recipients'],
},
})
.input(ZDeleteTemplateRecipientRequestSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { recipientId } = input;
await deleteTemplateRecipient({
recipientId,
userId: ctx.user.id,
teamId,
});
}),
/**
* @private
*/
setTemplateRecipients: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/recipient/set',
// summary: 'Set template recipients',
// description:
// 'This will replace all recipients attached to the template. If the array contains existing recipients, they will be updated and the original fields will be retained.',
// tags: ['Template Recipients'],
// },
// })
.input(ZSetTemplateRecipientsRequestSchema)
.output(ZSetTemplateRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipients } = input;
return await setTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients: recipients.map((recipient) => ({
id: recipient.nativeId,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
actionAuth: recipient.actionAuth,
})),
});
}),
@ -147,4 +464,32 @@ export const recipientRouter = router({
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
/**
* Leaving this here and will remove after deployment.
*
* @deprecated Remove after deployment.
*/
addSigners: authenticatedProcedure
.input(ZAddSignersMutationSchema)
.output(ZSetDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, signers } = input;
return await setDocumentRecipients({
userId: ctx.user.id,
documentId,
teamId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
requestMetadata: ctx.metadata,
});
}),
});

View File

@ -1,24 +1,114 @@
import { z } from 'zod';
import {
ZRecipientAccessAuthTypesSchema,
ZRecipientActionAuthSchema,
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { RecipientRole } from '@documenso/prisma/client';
import { FieldSchema, RecipientSchema } from '@documenso/prisma/generated/zod';
export const ZGetRecipientQuerySchema = z.object({
recipientId: z.number(),
teamId: z.number().optional(),
});
export const ZAddSignersMutationSchema = z
const ZCreateRecipientSchema = z.object({
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
});
const ZUpdateRecipientSchema = z.object({
id: z.number().describe('The ID of the recipient to update.'),
email: z.string().toLowerCase().email().min(1).optional(),
name: z.string().optional(),
role: z.nativeEnum(RecipientRole).optional(),
signingOrder: z.number().optional(),
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
});
/**
* Use this when returning base recipients from the API.
*/
export const ZRecipientBaseResponseSchema = RecipientSchema.pick({
id: true,
documentId: true,
templateId: true,
email: true,
name: true,
token: true,
documentDeletedAt: true,
expired: true,
signedAt: true,
authOptions: true,
signingOrder: true,
rejectionReason: true,
role: true,
readStatus: true,
signingStatus: true,
sendStatus: true,
});
/**
* Use this when returning a full recipient from the API.
*/
export const ZRecipientResponseSchema = ZRecipientBaseResponseSchema.extend({
Field: FieldSchema.array(),
});
export const ZCreateDocumentRecipientRequestSchema = z.object({
documentId: z.number(),
recipient: ZCreateRecipientSchema,
});
export const ZCreateDocumentRecipientResponseSchema = ZRecipientBaseResponseSchema;
export const ZCreateDocumentRecipientsRequestSchema = z.object({
documentId: z.number(),
recipients: z.array(ZCreateRecipientSchema).refine((recipients) => {
const emails = recipients.map((recipient) => recipient.email.toLowerCase());
return new Set(emails).size === emails.length;
}),
});
export const ZUpdateDocumentRecipientRequestSchema = z.object({
documentId: z.number(),
recipient: ZUpdateRecipientSchema,
});
export const ZUpdateDocumentRecipientResponseSchema = ZRecipientResponseSchema;
export const ZUpdateDocumentRecipientsRequestSchema = z.object({
documentId: z.number(),
recipients: z.array(ZUpdateRecipientSchema).refine((recipients) => {
const emails = recipients
.filter((recipient) => recipient.email !== undefined)
.map((recipient) => recipient.email?.toLowerCase());
return new Set(emails).size === emails.length;
}),
});
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
recipients: z.array(ZRecipientResponseSchema),
});
export const ZDeleteDocumentRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZSetDocumentRecipientsRequestSchema = z
.object({
documentId: z.number(),
teamId: z.number().optional(),
signers: z.array(
recipients: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().email().min(1),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@ -28,7 +118,7 @@ export const ZAddSignersMutationSchema = z
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
const emails = schema.recipients.map((recipient) => recipient.email.toLowerCase());
return new Set(emails).size === emails.length;
},
@ -36,16 +126,59 @@ export const ZAddSignersMutationSchema = z
{ message: 'Signers must have unique emails', path: ['signers__root'] },
);
export type TAddSignersMutationSchema = z.infer<typeof ZAddSignersMutationSchema>;
export const ZCreateTemplateRecipientRequestSchema = z.object({
templateId: z.number(),
recipient: ZCreateRecipientSchema,
});
export const ZAddTemplateSignersMutationSchema = z
export const ZCreateTemplateRecipientResponseSchema = ZRecipientBaseResponseSchema;
export const ZCreateTemplateRecipientsRequestSchema = z.object({
templateId: z.number(),
recipients: z.array(ZCreateRecipientSchema).refine((recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
}),
});
export const ZUpdateTemplateRecipientRequestSchema = z.object({
templateId: z.number(),
recipient: ZUpdateRecipientSchema,
});
export const ZUpdateTemplateRecipientResponseSchema = ZRecipientResponseSchema;
export const ZUpdateTemplateRecipientsRequestSchema = z.object({
templateId: z.number(),
recipients: z.array(ZUpdateRecipientSchema).refine((recipients) => {
const emails = recipients
.filter((recipient) => recipient.email !== undefined)
.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
}),
});
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
recipients: z.array(ZRecipientResponseSchema).refine((recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
}),
});
export const ZDeleteTemplateRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZSetTemplateRecipientsRequestSchema = z
.object({
teamId: z.number().optional(),
templateId: z.number(),
signers: z.array(
recipients: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().email().min(1),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@ -55,16 +188,14 @@ export const ZAddTemplateSignersMutationSchema = z
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
const emails = schema.recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
);
export type TAddTemplateSignersMutationSchema = z.infer<typeof ZAddTemplateSignersMutationSchema>;
export const ZCompleteDocumentWithTokenMutationSchema = z.object({
token: z.string(),
documentId: z.number(),
@ -85,3 +216,32 @@ export const ZRejectDocumentWithTokenMutationSchema = z.object({
export type TRejectDocumentWithTokenMutationSchema = z.infer<
typeof ZRejectDocumentWithTokenMutationSchema
>;
/**
* Legacy schema. Remove after deployment (when addSigners trpc is removed).
*
* @deprecated
*/
export const ZAddSignersMutationSchema = z
.object({
documentId: z.number(),
signers: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
}),
),
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
);

View File

@ -1,8 +1,6 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import {
ZGetDocumentWithDetailsByIdResponseSchema,
@ -45,10 +43,9 @@ import {
toggleTemplateDirectLink,
} from '@documenso/lib/server-only/template/toggle-template-direct-link';
import {
ZUpdateTemplateSettingsResponseSchema,
updateTemplateSettings,
} from '@documenso/lib/server-only/template/update-template-settings';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
ZUpdateTemplateResponseSchema,
updateTemplate,
} from '@documenso/lib/server-only/template/update-template';
import type { Document } from '@documenso/prisma/client';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
@ -63,10 +60,8 @@ import {
ZFindTemplatesQuerySchema,
ZGetTemplateByIdQuerySchema,
ZMoveTemplatesToTeamSchema,
ZSetSigningOrderForTemplateMutationSchema,
ZToggleTemplateDirectLinkMutationSchema,
ZUpdateTemplateSettingsMutationSchema,
ZUpdateTemplateTypedSignatureSettingsMutationSchema,
ZUpdateTemplateRequestSchema,
} from './schema';
export const templateRouter = router({
@ -77,7 +72,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'GET',
path: '/template/find',
path: '/template',
summary: 'Find templates',
description: 'Find templates based on a search criteria',
tags: ['Template'],
@ -86,8 +81,11 @@ export const templateRouter = router({
.input(ZFindTemplatesQuerySchema)
.output(ZFindTemplatesResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
return await findTemplates({
userId: ctx.user.id,
teamId,
...input,
});
}),
@ -107,7 +105,8 @@ export const templateRouter = router({
.input(ZGetTemplateByIdQuerySchema)
.output(ZGetTemplateByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const { teamId } = ctx;
const { templateId } = input;
return await getTemplateById({
id: templateId,
@ -117,22 +116,25 @@ export const templateRouter = router({
}),
/**
* @public
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/create',
summary: 'Create template',
description: 'Create a new template',
tags: ['Template'],
},
})
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/create',
// summary: 'Create template',
// description: 'Create a new template',
// tags: ['Template'],
// },
// })
.input(ZCreateTemplateMutationSchema)
.output(ZCreateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, title, templateDocumentDataId } = input;
const { teamId } = ctx;
const { title, templateDocumentDataId } = input;
return await createTemplate({
userId: ctx.user.id,
@ -149,30 +151,25 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}',
path: '/template/update',
summary: 'Update template',
tags: ['Template'],
},
})
.input(ZUpdateTemplateSettingsMutationSchema)
.output(ZUpdateTemplateSettingsResponseSchema)
.input(ZUpdateTemplateRequestSchema)
.output(ZUpdateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, data, meta } = input;
const { teamId } = ctx;
const { templateId, data, meta } = input;
const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await updateTemplateSettings({
return await updateTemplate({
userId,
teamId,
templateId,
data,
meta: {
...meta,
language: isValidLanguageCode(meta?.language) ? meta?.language : undefined,
},
requestMetadata,
meta,
});
}),
@ -183,7 +180,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/duplicate',
path: '/template/duplicate',
summary: 'Duplicate template',
tags: ['Template'],
},
@ -191,7 +188,8 @@ export const templateRouter = router({
.input(ZDuplicateTemplateMutationSchema)
.output(ZDuplicateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, templateId } = input;
const { teamId } = ctx;
const { templateId } = input;
return await duplicateTemplate({
userId: ctx.user.id,
@ -206,8 +204,8 @@ export const templateRouter = router({
deleteTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/delete',
method: 'DELETE',
path: '/template/{templateId}',
summary: 'Delete template',
tags: ['Template'],
},
@ -215,7 +213,8 @@ export const templateRouter = router({
.input(ZDeleteTemplateMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
@ -229,7 +228,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/use',
path: '/template/use',
summary: 'Use template',
description: 'Use the template to create a document',
tags: ['Template'],
@ -238,7 +237,8 @@ export const templateRouter = router({
.input(ZCreateDocumentFromTemplateMutationSchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.mutation(async ({ ctx, input }) => {
const { templateId, teamId, recipients, distributeDocument, customDocumentDataId } = input;
const { teamId } = ctx;
const { templateId, recipients, distributeDocument, customDocumentDataId } = input;
const limits = await getServerLimits({ email: ctx.user.email, teamId });
@ -246,15 +246,13 @@ export const templateRouter = router({
throw new Error('You have reached your document limit.');
}
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
const document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
recipients,
customDocumentDataId,
requestMetadata,
requestMetadata: ctx.metadata,
});
if (distributeDocument) {
@ -262,7 +260,7 @@ export const templateRouter = router({
documentId: document.id,
userId: ctx.user.id,
teamId,
requestMetadata,
requestMetadata: ctx.metadata,
}).catch((err) => {
console.error(err);
@ -278,18 +276,20 @@ export const templateRouter = router({
}),
/**
* @public
* Leaving this endpoint as private for now until there is a use case for it.
*
* @private
*/
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/use',
summary: 'Use direct template',
description: 'Use a direct template to create a document',
tags: ['Template'],
},
})
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/direct/use',
// summary: 'Use direct template',
// description: 'Use a direct template to create a document',
// tags: ['Template'],
// },
// })
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
.output(ZCreateDocumentFromDirectTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
@ -302,8 +302,6 @@ export const templateRouter = router({
templateUpdatedAt,
} = input;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
@ -318,25 +316,7 @@ export const templateRouter = router({
email: ctx.user.email,
}
: undefined,
requestMetadata,
});
}),
/**
* @private
*/
setSigningOrderForTemplate: authenticatedProcedure
.input(ZSetSigningOrderForTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, signingOrder } = input;
return await updateTemplateSettings({
templateId,
teamId,
data: {},
meta: { signingOrder },
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata,
});
}),
@ -347,7 +327,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/direct/create',
path: '/template/direct/create',
summary: 'Create direct link',
description: 'Create a direct link for a template',
tags: ['Template'],
@ -356,7 +336,8 @@ export const templateRouter = router({
.input(ZCreateTemplateDirectLinkMutationSchema)
.output(ZCreateTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, directRecipientId } = input;
const { teamId } = ctx;
const { templateId, directRecipientId } = input;
const userId = ctx.user.id;
@ -370,7 +351,7 @@ export const templateRouter = router({
});
}
return await createTemplateDirectLink({ userId, templateId, directRecipientId });
return await createTemplateDirectLink({ userId, teamId, templateId, directRecipientId });
}),
/**
@ -379,8 +360,8 @@ export const templateRouter = router({
deleteTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/direct/delete',
method: 'DELETE',
path: '/template/direct/{templateId}',
summary: 'Delete direct link',
description: 'Delete a direct link for a template',
tags: ['Template'],
@ -389,11 +370,12 @@ export const templateRouter = router({
.input(ZDeleteTemplateDirectLinkMutationSchema)
.output(z.void())
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
await deleteTemplateDirectLink({ userId, templateId });
await deleteTemplateDirectLink({ userId, teamId, templateId });
}),
/**
@ -412,11 +394,12 @@ export const templateRouter = router({
.input(ZToggleTemplateDirectLinkMutationSchema)
.output(ZToggleTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, enabled } = input;
const userId = ctx.user.id;
return await toggleTemplateDirectLink({ userId, templateId, enabled });
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
}),
/**
@ -426,7 +409,7 @@ export const templateRouter = router({
.meta({
openapi: {
method: 'POST',
path: '/template/{templateId}/move',
path: '/template/move',
summary: 'Move template',
description: 'Move a template to a team',
tags: ['Template'],
@ -444,37 +427,4 @@ export const templateRouter = router({
userId,
});
}),
/**
* @private
*/
updateTemplateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTemplateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId, typedSignatureEnabled } = input;
const template = await getTemplateById({
id: templateId,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!template) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Template not found',
});
}
return await updateTemplateSettings({
templateId,
teamId,
userId: ctx.user.id,
data: {},
meta: {
typedSignatureEnabled,
},
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@ -1,25 +1,27 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import {
DocumentDistributionMethod,
DocumentSigningOrder,
DocumentVisibility,
TemplateType,
} from '@documenso/prisma/client';
import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@documenso/prisma/client';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
teamId: z.number().optional(),
templateDocumentDataId: z.string().min(1),
});
@ -34,119 +36,119 @@ export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
recipients: z
.array(
z.object({
id: z.number(),
id: z.number().describe('The ID of the recipient in the template.'),
email: z.string().email(),
name: z.string().optional(),
}),
)
.describe('The information of the recipients to create the document with.')
.refine((recipients) => {
const emails = recipients.map((signer) => signer.email);
return new Set(emails).size === emails.length;
}, 'Recipients must have unique emails'),
distributeDocument: z.boolean().optional(),
customDocumentDataId: z.string().optional(),
distributeDocument: z
.boolean()
.describe('Whether to create the document as pending and distribute it to recipients.')
.optional(),
customDocumentDataId: z
.string()
.describe(
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
)
.optional(),
});
export const ZDuplicateTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
});
export const ZCreateTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
directRecipientId: z.number().min(1).optional(),
templateId: z.number(),
directRecipientId: z
.number()
.describe(
'The of the recipient in the current template to transform into the primary recipient when the template is used.',
)
.optional(),
});
export const ZDeleteTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
templateId: z.number(),
});
export const ZToggleTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
templateId: z.number(),
enabled: z.boolean(),
});
export const ZDeleteTemplateMutationSchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
templateId: z.number(),
});
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
export const ZUpdateTemplateSettingsMutationSchema = z.object({
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
teamId: z.number().min(1).optional(),
data: z.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.optional(),
type: z.nativeEnum(TemplateType).optional(),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
}),
data: z
.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
type: z.nativeEnum(TemplateType).optional(),
})
.optional(),
meta: z
.object({
subject: z.string(),
message: z.string(),
timezone: z.string(),
dateFormat: z.string(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
emailSettings: ZDocumentEmailSettingsSchema,
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
typedSignatureEnabled: z.boolean().optional(),
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
})
.optional(),
});
export const ZSetSigningOrderForTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder),
});
export const ZFindTemplatesQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number().optional(),
type: z.nativeEnum(TemplateType).optional(),
type: z.nativeEnum(TemplateType).describe('Filter templates by type.').optional(),
});
export const ZGetTemplateByIdQuerySchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
});
export const ZMoveTemplatesToTeamSchema = z.object({
templateId: z.number(),
teamId: z.number(),
});
export const ZUpdateTemplateTypedSignatureSettingsMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
typedSignatureEnabled: z.boolean(),
templateId: z.number().describe('The ID of the template to move to.'),
teamId: z.number().describe('The ID of the team to move the template to.'),
});
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;

View File

@ -5,6 +5,8 @@ import type { OpenApiMeta } from 'trpc-openapi';
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
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 { TrpcContext } from './context';
@ -62,8 +64,23 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
ctx: {
...ctx,
user: apiToken.user,
teamId: apiToken.teamId || undefined,
session: null,
source: 'api',
metadata: {
...ctx.metadata,
auditUser: apiToken.team
? {
id: null,
email: null,
name: apiToken.team.name,
}
: {
id: apiToken.user.id,
email: apiToken.user.email,
name: apiToken.user.name,
},
auth: 'api',
} satisfies ApiRequestMetadata,
},
});
}
@ -71,7 +88,7 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You must be logged in to perform this action.',
message: 'Invalid session or API token.',
});
}
@ -80,17 +97,39 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
...ctx,
user: ctx.user,
session: ctx.session,
source: 'app',
metadata: {
...ctx.metadata,
auditUser: {
id: ctx.user.id,
name: ctx.user.name,
email: ctx.user.email,
},
auth: 'session',
} satisfies ApiRequestMetadata,
},
});
});
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await next({
ctx: {
...ctx,
user: ctx.user,
session: ctx.session,
metadata: {
...ctx.metadata,
auditUser: ctx.user
? {
id: ctx.user.id,
name: ctx.user.name,
email: ctx.user.email,
}
: undefined,
requestMetadata,
auth: ctx.session ? 'session' : null,
} satisfies ApiRequestMetadata,
},
});
});
@ -117,6 +156,15 @@ export const adminMiddleware = t.middleware(async ({ ctx, next }) => {
...ctx,
user: ctx.user,
session: ctx.session,
metadata: {
...ctx.metadata,
auditUser: {
id: ctx.user.id,
name: ctx.user.name,
email: ctx.user.email,
},
auth: 'session',
} satisfies ApiRequestMetadata,
},
});
});

View File

@ -2,7 +2,6 @@ import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/d
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-codes';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, router } from '../trpc';
import {
@ -28,7 +27,7 @@ export const twoFactorAuthenticationRouter = router({
return await enableTwoFactorAuthentication({
user,
code,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),
@ -41,7 +40,7 @@ export const twoFactorAuthenticationRouter = router({
user,
totpCode: input.totpCode,
backupCode: input.backupCode,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
requestMetadata: ctx.metadata.requestMetadata,
});
}),

View File

@ -2,9 +2,6 @@
import { forwardRef, useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
import { usePathname } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
@ -25,7 +22,6 @@ import {
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { useToast } from '@documenso/ui/primitives/use-toast';
import type { FieldFormType } from './add-fields';
@ -146,50 +142,14 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSettingsProps>(
(
{
title,
description,
field,
fields,
onAdvancedSettings,
isDocumentPdfLoaded = true,
onSave,
teamId,
},
{ title, description, field, fields, onAdvancedSettings, isDocumentPdfLoaded = true, onSave },
ref,
) => {
const { _ } = useLingui();
const { toast } = useToast();
const params = useParams();
const pathname = usePathname();
const id = params?.id;
const isTemplatePage = pathname?.includes('template');
const isDocumentPage = pathname?.includes('document');
const [errors, setErrors] = useState<string[]>([]);
const { data: template } = trpc.template.getTemplateById.useQuery(
{
templateId: Number(id),
teamId,
},
{
enabled: isTemplatePage,
},
);
const { data: document } = trpc.document.getDocumentById.useQuery(
{
documentId: Number(id),
teamId,
},
{
enabled: isDocumentPage,
},
);
const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined;
const fieldMeta = field?.fieldMeta;
const localStorageKey = `field_${field.formId}_${field.type}`;