diff --git a/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts b/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts index e8bbfa92f..ca26e1f23 100644 --- a/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts +++ b/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts @@ -28,9 +28,13 @@ import { seedPendingDocument, } from '@documenso/prisma/seed/documents'; import { seedBlankFolder } from '@documenso/prisma/seed/folders'; -import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; +import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates'; import { seedUser } from '@documenso/prisma/seed/users'; import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types'; +import type { + TUseEnvelopePayload, + TUseEnvelopeResponse, +} from '@documenso/trpc/server/envelope-router/use-envelope.types'; const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); @@ -3074,6 +3078,82 @@ test.describe('Document API V2', () => { }); }); + test.describe('Envelope use endpoint', () => { + test('should block unauthorized access to envelope use endpoint', async ({ request }) => { + const doc = await seedTemplate({ + title: 'Team template 1', + userId: userA.id, + teamId: teamA.id, + internalVersion: 2, + }); + + const payload: TUseEnvelopePayload = { + envelopeId: doc.id, + }; + + const formData = new FormData(); + formData.append('payload', JSON.stringify(payload)); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/use`, { + headers: { Authorization: `Bearer ${tokenB}` }, + multipart: formData, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to envelope use endpoint', async ({ page, request }) => { + const doc = await seedTemplate({ + title: 'Team template 1', + userId: userA.id, + teamId: teamA.id, + internalVersion: 2, + }); + + const payload: TUseEnvelopePayload = { + envelopeId: doc.id, + distributeDocument: true, + recipients: [ + { + id: doc.recipients[0].id, + email: doc.recipients[0].email, + name: 'New Name', + }, + ], + }; + + const formData = new FormData(); + formData.append('payload', JSON.stringify(payload)); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/use`, { + headers: { Authorization: `Bearer ${tokenA}` }, + multipart: formData, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + + const data: TUseEnvelopeResponse = await res.json(); + + const createdEnvelope = await prisma.envelope.findFirst({ + where: { + id: data.id, + }, + include: { + recipients: true, + }, + }); + + expect(createdEnvelope).toBeDefined(); + expect(createdEnvelope?.recipients.length).toBe(1); + expect(createdEnvelope?.recipients[0].email).toBe(doc.recipients[0].email); + expect(createdEnvelope?.recipients[0].name).toBe('New Name'); + expect(createdEnvelope?.recipients[0].token).toBe(data.recipients[0].token); + expect(createdEnvelope?.recipients[0].token).not.toBe(doc.recipients[0].token); + }); + }); + test.describe('Envelope distribute endpoint', () => { test('should block unauthorized access to envelope distribute endpoint', async ({ request, diff --git a/packages/lib/server-only/document/resend-document.ts b/packages/lib/server-only/document/resend-document.ts index 6307eff5a..1ae5f40d0 100644 --- a/packages/lib/server-only/document/resend-document.ts +++ b/packages/lib/server-only/document/resend-document.ts @@ -44,7 +44,7 @@ export const resendDocument = async ({ recipients, teamId, requestMetadata, -}: ResendDocumentOptions): Promise => { +}: ResendDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, @@ -103,7 +103,7 @@ export const resendDocument = async ({ ).recipientSigningRequest; if (!isRecipientSigningRequestEmailEnabled) { - return; + return envelope; } const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } = @@ -230,4 +230,6 @@ export const resendDocument = async ({ ); }), ); + + return envelope; }; diff --git a/packages/prisma/seed/templates.ts b/packages/prisma/seed/templates.ts index c83dfa1d9..cf3113a1c 100644 --- a/packages/prisma/seed/templates.ts +++ b/packages/prisma/seed/templates.ts @@ -110,7 +110,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => { data: { id: prefixedId('envelope'), secondaryId: templateId.formattedTemplateId, - internalVersion: 1, + internalVersion: options.internalVersion ?? 1, type: EnvelopeType.TEMPLATE, title, envelopeItems: { @@ -143,6 +143,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => { documentData: true, }, }, + recipients: true, }, }); }; diff --git a/packages/trpc/server/envelope-router/distribute-envelope.ts b/packages/trpc/server/envelope-router/distribute-envelope.ts index 8f1db3fa1..baa16caec 100644 --- a/packages/trpc/server/envelope-router/distribute-envelope.ts +++ b/packages/trpc/server/envelope-router/distribute-envelope.ts @@ -1,7 +1,7 @@ import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; +import { formatSigningLink } from '@documenso/lib/utils/recipients'; -import { ZGenericSuccessResponse } from '../schema'; import { authenticatedProcedure } from '../trpc'; import { ZDistributeEnvelopeRequestSchema, @@ -45,7 +45,7 @@ export const distributeEnvelopeRoute = authenticatedProcedure }); } - await sendDocument({ + const envelope = await sendDocument({ userId: ctx.user.id, id: { type: 'envelopeId', @@ -55,5 +55,17 @@ export const distributeEnvelopeRoute = authenticatedProcedure requestMetadata: ctx.metadata, }); - return ZGenericSuccessResponse; + return { + success: true, + id: envelope.id, + recipients: envelope.recipients.map((recipient) => ({ + id: recipient.id, + name: recipient.name, + email: recipient.email, + token: recipient.token, + role: recipient.role, + signingOrder: recipient.signingOrder, + signingUrl: formatSigningLink(recipient.token), + })), + }; }); diff --git a/packages/trpc/server/envelope-router/distribute-envelope.types.ts b/packages/trpc/server/envelope-router/distribute-envelope.types.ts index 2f650f9bd..a43a6fc2d 100644 --- a/packages/trpc/server/envelope-router/distribute-envelope.types.ts +++ b/packages/trpc/server/envelope-router/distribute-envelope.types.ts @@ -4,6 +4,7 @@ import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta'; import { ZSuccessResponseSchema } from '../schema'; import type { TrpcRouteMeta } from '../trpc'; +import { ZRecipientWithSigningUrlSchema } from './schema'; export const distributeEnvelopeMeta: TrpcRouteMeta = { openapi: { @@ -31,7 +32,10 @@ export const ZDistributeEnvelopeRequestSchema = z.object({ }).optional(), }); -export const ZDistributeEnvelopeResponseSchema = ZSuccessResponseSchema; +export const ZDistributeEnvelopeResponseSchema = ZSuccessResponseSchema.extend({ + id: z.string().describe('The ID of the envelope that was sent.'), + recipients: ZRecipientWithSigningUrlSchema.array(), +}); export type TDistributeEnvelopeRequest = z.infer; export type TDistributeEnvelopeResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/redistribute-envelope.ts b/packages/trpc/server/envelope-router/redistribute-envelope.ts index 1fc41194a..ae31b59a3 100644 --- a/packages/trpc/server/envelope-router/redistribute-envelope.ts +++ b/packages/trpc/server/envelope-router/redistribute-envelope.ts @@ -1,6 +1,6 @@ import { resendDocument } from '@documenso/lib/server-only/document/resend-document'; +import { formatSigningLink } from '@documenso/lib/utils/recipients'; -import { ZGenericSuccessResponse } from '../schema'; import { authenticatedProcedure } from '../trpc'; import { ZRedistributeEnvelopeRequestSchema, @@ -23,7 +23,7 @@ export const redistributeEnvelopeRoute = authenticatedProcedure }, }); - await resendDocument({ + const envelope = await resendDocument({ userId: ctx.user.id, teamId, id: { @@ -34,5 +34,17 @@ export const redistributeEnvelopeRoute = authenticatedProcedure requestMetadata: ctx.metadata, }); - return ZGenericSuccessResponse; + return { + success: true, + id: envelope.id, + recipients: envelope.recipients.map((recipient) => ({ + id: recipient.id, + name: recipient.name, + email: recipient.email, + token: recipient.token, + role: recipient.role, + signingOrder: recipient.signingOrder, + signingUrl: formatSigningLink(recipient.token), + })), + }; }); diff --git a/packages/trpc/server/envelope-router/redistribute-envelope.types.ts b/packages/trpc/server/envelope-router/redistribute-envelope.types.ts index 94e63b237..89befc281 100644 --- a/packages/trpc/server/envelope-router/redistribute-envelope.types.ts +++ b/packages/trpc/server/envelope-router/redistribute-envelope.types.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { ZSuccessResponseSchema } from '../schema'; import type { TrpcRouteMeta } from '../trpc'; +import { ZRecipientWithSigningUrlSchema } from './schema'; export const redistributeEnvelopeMeta: TrpcRouteMeta = { openapi: { @@ -22,7 +23,10 @@ export const ZRedistributeEnvelopeRequestSchema = z.object({ .describe('The IDs of the recipients to redistribute the envelope to.'), }); -export const ZRedistributeEnvelopeResponseSchema = ZSuccessResponseSchema; +export const ZRedistributeEnvelopeResponseSchema = ZSuccessResponseSchema.extend({ + id: z.string().describe('The ID of the envelope that was redistributed.'), + recipients: ZRecipientWithSigningUrlSchema.array(), +}); export type TRedistributeEnvelopeRequest = z.infer; export type TRedistributeEnvelopeResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/schema.ts b/packages/trpc/server/envelope-router/schema.ts new file mode 100644 index 000000000..a2245f73e --- /dev/null +++ b/packages/trpc/server/envelope-router/schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +import RecipientSchema from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema'; + +// Common schemas between envelope routes. + +export const ZRecipientWithSigningUrlSchema = RecipientSchema.pick({ + id: true, + name: true, + email: true, + token: true, + role: true, + signingOrder: true, +}).extend({ + signingUrl: z.string().describe('The URL which the recipient uses to sign the document.'), +}); diff --git a/packages/trpc/server/envelope-router/use-envelope.ts b/packages/trpc/server/envelope-router/use-envelope.ts index fee863e94..ebe6af280 100644 --- a/packages/trpc/server/envelope-router/use-envelope.ts +++ b/packages/trpc/server/envelope-router/use-envelope.ts @@ -6,6 +6,7 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document' import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; +import { formatSigningLink } from '@documenso/lib/utils/recipients'; import { authenticatedProcedure } from '../trpc'; import { @@ -26,7 +27,7 @@ export const useEnvelopeRoute = authenticatedProcedure const { envelopeId, externalId, - recipients, + recipients = [], distributeDocument, customDocumentData = [], folderId, @@ -166,5 +167,14 @@ export const useEnvelopeRoute = authenticatedProcedure return { id: createdEnvelope.id, + recipients: createdEnvelope.recipients.map((recipient) => ({ + id: recipient.id, + name: recipient.name, + email: recipient.email, + token: recipient.token, + role: recipient.role, + signingOrder: recipient.signingOrder, + signingUrl: formatSigningLink(recipient.token), + })), }; }); diff --git a/packages/trpc/server/envelope-router/use-envelope.types.ts b/packages/trpc/server/envelope-router/use-envelope.types.ts index 9150f1915..46d40103b 100644 --- a/packages/trpc/server/envelope-router/use-envelope.types.ts +++ b/packages/trpc/server/envelope-router/use-envelope.types.ts @@ -19,6 +19,7 @@ import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta'; import { zodFormData } from '../../utils/zod-form-data'; import type { TrpcRouteMeta } from '../trpc'; +import { ZRecipientWithSigningUrlSchema } from './schema'; export const useEnvelopeMeta: TrpcRouteMeta = { openapi: { @@ -44,7 +45,8 @@ export const ZUseEnvelopePayloadSchema = z.object({ signingOrder: z.number().optional(), }), ) - .describe('The information of the recipients to create the document with.'), + .describe('The information of the recipients to create the document with.') + .optional(), distributeDocument: z .boolean() .describe('Whether to create the document as pending and distribute it to recipients.') @@ -114,6 +116,7 @@ export const ZUseEnvelopeRequestSchema = zodFormData({ export const ZUseEnvelopeResponseSchema = z.object({ id: z.string().describe('The ID of the created envelope.'), + recipients: ZRecipientWithSigningUrlSchema.array(), }); export type TUseEnvelopePayload = z.infer;