feat: add template and field endpoints (#1572)

This commit is contained in:
David Nguyen
2025-01-11 15:33:20 +11:00
committed by GitHub
parent 6520bbd5e3
commit ebbe922982
92 changed files with 3920 additions and 1396 deletions

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,
});
}),