Merge branch 'main' into feat/unlink-documents-deleted-org

This commit is contained in:
Lucas Smith
2025-10-28 21:04:54 +11:00
committed by GitHub
582 changed files with 47578 additions and 11614 deletions

View File

@ -10,7 +10,7 @@ export const createSubscriptionClaimRoute = adminProcedure
.input(ZCreateSubscriptionClaimRequestSchema)
.output(ZCreateSubscriptionClaimResponseSchema)
.mutation(async ({ input, ctx }) => {
const { name, teamCount, memberCount, flags } = input;
const { name, teamCount, memberCount, envelopeItemCount, flags } = input;
ctx.logger.info({
input,
@ -20,6 +20,7 @@ export const createSubscriptionClaimRoute = adminProcedure
data: {
name,
teamCount,
envelopeItemCount,
memberCount,
flags,
},

View File

@ -6,6 +6,7 @@ export const ZCreateSubscriptionClaimRequestSchema = z.object({
name: z.string().min(1),
teamCount: z.number().int().min(0),
memberCount: z.number().int().min(0),
envelopeItemCount: z.number().int().min(1),
flags: ZClaimFlagsSchema,
});

View File

@ -1,5 +1,5 @@
import { adminSuperDeleteDocument } from '@documenso/lib/server-only/admin/admin-super-delete-document';
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
import { adminProcedure } from '../trpc';
import {
@ -19,10 +19,10 @@ export const deleteDocumentRoute = adminProcedure
},
});
await sendDeleteEmail({ documentId: id, reason });
await sendDeleteEmail({ envelopeId: id, reason });
await superDeleteDocument({
id,
await adminSuperDeleteDocument({
envelopeId: id,
requestMetadata: ctx.metadata.requestMetadata,
});
});

View File

@ -1,7 +1,7 @@
import { z } from 'zod';
export const ZDeleteDocumentRequestSchema = z.object({
id: z.number().min(1),
id: z.string(),
reason: z.string(),
});

View File

@ -0,0 +1,72 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import {
mapSecondaryIdToDocumentId,
unsafeBuildEnvelopeIdQuery,
} from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZFindDocumentJobsRequestSchema,
ZFindDocumentJobsResponseSchema,
} from './find-document-jobs.types';
export const findDocumentJobsRoute = adminProcedure
.input(ZFindDocumentJobsRequestSchema)
.output(ZFindDocumentJobsResponseSchema)
.query(async ({ input }) => {
const { envelopeId, page = 1, perPage = 5 } = input;
const envelope = await prisma.envelope.findFirst({
where: unsafeBuildEnvelopeIdQuery(
{
type: 'envelopeId',
id: envelopeId,
},
EnvelopeType.DOCUMENT,
),
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const [data, count] = await Promise.all([
prisma.backgroundJob.findMany({
where: {
jobId: 'internal.seal-document',
payload: {
path: ['documentId'],
equals: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
},
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
submittedAt: 'desc',
},
}),
prisma.backgroundJob.count({
where: {
jobId: 'internal.seal-document',
payload: {
path: ['documentId'],
equals: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
},
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
});

View File

@ -0,0 +1,26 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import BackgroundJobSchema from '@documenso/prisma/generated/zod/modelSchema/BackgroundJobSchema';
export const ZFindDocumentJobsRequestSchema = ZFindSearchParamsSchema.extend({
envelopeId: z.string(),
});
export const ZFindDocumentJobsResponseSchema = ZFindResultResponse.extend({
data: BackgroundJobSchema.pick({
status: true,
id: true,
retried: true,
maxRetries: true,
jobId: true,
name: true,
version: true,
submittedAt: true,
updatedAt: true,
completedAt: true,
lastRetriedAt: true,
}).array(),
});
export type TFindDocumentJobsResponse = z.infer<typeof ZFindDocumentJobsResponseSchema>;

View File

@ -1,4 +1,5 @@
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { adminFindDocuments } from '@documenso/lib/server-only/admin/admin-find-documents';
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
import { adminProcedure } from '../trpc';
import { ZFindDocumentsRequestSchema, ZFindDocumentsResponseSchema } from './find-documents.types';
@ -9,5 +10,10 @@ export const findDocumentsRoute = adminProcedure
.query(async ({ input }) => {
const { query, page, perPage } = input;
return await findDocuments({ query, page, perPage });
const result = await adminFindDocuments({ query, page, perPage });
return {
...result,
data: result.data.map(mapEnvelopesToDocumentMany),
};
});

View File

@ -13,6 +13,7 @@ export const ZFindSubscriptionClaimsResponseSchema = ZFindResultResponse.extend(
name: true,
teamCount: true,
memberCount: true,
envelopeItemCount: true,
locked: true,
flags: true,
}).array(),

View File

@ -0,0 +1,124 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZPromoteMemberToOwnerRequestSchema,
ZPromoteMemberToOwnerResponseSchema,
} from './promote-member-to-owner.types';
export const promoteMemberToOwnerRoute = adminProcedure
.input(ZPromoteMemberToOwnerRequestSchema)
.output(ZPromoteMemberToOwnerResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, userId } = input;
ctx.logger.info({
input: {
organisationId,
userId,
},
});
// First, verify the organisation exists and get member details with groups
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
groups: {
where: {
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
},
members: {
where: {
userId,
},
include: {
organisationGroupMembers: {
include: {
group: true,
},
},
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
// Verify the user is a member of the organisation
const [member] = organisation.members;
if (!member) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User is not a member of this organisation',
});
}
// Verify the user is not already the owner
if (organisation.ownerUserId === userId) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'User is already the owner of this organisation',
});
}
// Get current organisation role
const currentOrganisationRole = getHighestOrganisationRoleInGroup(
member.organisationGroupMembers.flatMap((member) => member.group),
);
// Find the current and target organisation groups
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentOrganisationRole,
);
const adminGroup = organisation.groups.find(
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
);
if (!currentMemberGroup) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Current member group not found',
});
}
if (!adminGroup) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Admin group not found',
});
}
// Update the organisation owner and member role in a transaction
await prisma.$transaction(async (tx) => {
// Update the organisation to set the new owner
await tx.organisation.update({
where: {
id: organisationId,
},
data: {
ownerUserId: userId,
},
});
// Only update role if the user is not already an admin then add them to the admin group
if (currentOrganisationRole !== OrganisationMemberRole.ADMIN) {
await tx.organisationGroupMember.create({
data: {
id: generateDatabaseId('group_member'),
organisationMemberId: member.id,
groupId: adminGroup.id,
},
});
}
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZPromoteMemberToOwnerRequestSchema = z.object({
organisationId: z.string().min(1),
userId: z.number().min(1),
});
export const ZPromoteMemberToOwnerResponseSchema = z.void();
export type TPromoteMemberToOwnerRequest = z.infer<typeof ZPromoteMemberToOwnerRequestSchema>;
export type TPromoteMemberToOwnerResponse = z.infer<typeof ZPromoteMemberToOwnerResponseSchema>;

View File

@ -1,6 +1,9 @@
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
import { EnvelopeType } from '@prisma/client';
import { jobs } from '@documenso/lib/jobs/client';
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { adminProcedure } from '../trpc';
import {
@ -20,9 +23,21 @@ export const resealDocumentRoute = adminProcedure
},
});
const document = await getEntireDocument({ id });
const envelope = await unsafeGetEntireEnvelope({
id: {
type: 'envelopeId',
id,
},
type: EnvelopeType.DOCUMENT,
});
const isResealing = isDocumentCompleted(document.status);
const isResealing = isDocumentCompleted(envelope.status);
await sealDocument({ documentId: id, isResealing });
await jobs.triggerJob({
name: 'internal.seal-document',
payload: {
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
isResealing,
},
});
});

View File

@ -1,7 +1,7 @@
import { z } from 'zod';
export const ZResealDocumentRequestSchema = z.object({
id: z.number().min(1),
id: z.string(),
});
export const ZResealDocumentResponseSchema = z.void();

View File

@ -8,10 +8,12 @@ import { deleteUserRoute } from './delete-user';
import { disableUserRoute } from './disable-user';
import { enableUserRoute } from './enable-user';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
import { findDocumentJobsRoute } from './find-document-jobs';
import { findDocumentsRoute } from './find-documents';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
import { getAdminOrganisationRoute } from './get-admin-organisation';
import { getUserRoute } from './get-user';
import { promoteMemberToOwnerRoute } from './promote-member-to-owner';
import { resealDocumentRoute } from './reseal-document';
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
import { updateAdminOrganisationRoute } from './update-admin-organisation';
@ -27,6 +29,9 @@ export const adminRouter = router({
create: createAdminOrganisationRoute,
update: updateAdminOrganisationRoute,
},
organisationMember: {
promoteToOwner: promoteMemberToOwnerRoute,
},
claims: {
find: findSubscriptionClaimsRoute,
create: createSubscriptionClaimRoute,
@ -48,6 +53,7 @@ export const adminRouter = router({
find: findDocumentsRoute,
delete: deleteDocumentRoute,
reseal: resealDocumentRoute,
findJobs: findDocumentJobsRoute,
},
recipient: {
update: updateRecipientRoute,

View File

@ -12,6 +12,7 @@ export const ZUpdateAdminOrganisationRequestSchema = z.object({
claims: ZCreateSubscriptionClaimRequestSchema.pick({
teamCount: true,
memberCount: true,
envelopeItemCount: true,
flags: true,
}).optional(),
customerId: z.string().optional(),

View File

@ -0,0 +1,96 @@
import { EnvelopeType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { DateTime } from 'luxon';
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '@documenso/lib/server-only/2fa/email/constants';
import { send2FATokenEmail } from '@documenso/lib/server-only/2fa/email/send-2fa-token-email';
import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
ZAccessAuthRequest2FAEmailRequestSchema,
ZAccessAuthRequest2FAEmailResponseSchema,
} from './access-auth-request-2fa-email.types';
export const accessAuthRequest2FAEmailRoute = procedure
.input(ZAccessAuthRequest2FAEmailRequestSchema)
.output(ZAccessAuthRequest2FAEmailResponseSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token } = input;
const user = ctx.user;
// Get document and recipient by token
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.DOCUMENT,
recipients: {
some: {
token,
},
},
},
include: {
recipients: {
where: {
token,
},
},
},
});
if (!envelope) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Document not found',
});
}
const [recipient] = envelope.recipients;
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
recipientAuth: recipient.authOptions,
});
if (!derivedRecipientAccessAuth.includes(DocumentAuth.TWO_FACTOR_AUTH)) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: '2FA is not required for this document',
});
}
// if (user && recipient.email !== user.email) {
// throw new TRPCError({
// code: 'UNAUTHORIZED',
// message: 'User does not match recipient',
// });
// }
const expiresAt = DateTime.now().plus({ minutes: TWO_FACTOR_EMAIL_EXPIRATION_MINUTES });
await send2FATokenEmail({
token,
envelopeId: envelope.id,
});
return {
success: true,
expiresAt: expiresAt.toJSDate(),
};
} catch (error) {
console.error('Error sending access auth 2FA email:', error);
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to send 2FA email',
});
}
});

View File

@ -0,0 +1,17 @@
import { z } from 'zod';
export const ZAccessAuthRequest2FAEmailRequestSchema = z.object({
token: z.string().min(1),
});
export const ZAccessAuthRequest2FAEmailResponseSchema = z.object({
success: z.boolean(),
expiresAt: z.date(),
});
export type TAccessAuthRequest2FAEmailRequest = z.infer<
typeof ZAccessAuthRequest2FAEmailRequestSchema
>;
export type TAccessAuthRequest2FAEmailResponse = z.infer<
typeof ZAccessAuthRequest2FAEmailResponseSchema
>;

View File

@ -0,0 +1,50 @@
import { EnvelopeType } from '@prisma/client';
import { createAttachment } from '@documenso/lib/server-only/envelope-attachment/create-attachment';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateAttachmentRequestSchema,
ZCreateAttachmentResponseSchema,
} from './create-attachment.types';
export const createAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/attachment/create',
summary: 'Create attachment',
description: 'Create a new attachment for a document',
tags: ['Document'],
},
})
.input(ZCreateAttachmentRequestSchema)
.output(ZCreateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { documentId, data } = input;
ctx.logger.info({
input: { documentId, label: data.label },
});
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
userId,
teamId,
type: EnvelopeType.DOCUMENT,
});
await createAttachment({
envelopeId: envelope.id,
teamId,
userId,
data,
});
});

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZCreateAttachmentRequestSchema = z.object({
documentId: z.number(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZCreateAttachmentResponseSchema = z.void();
export type TCreateAttachmentRequest = z.infer<typeof ZCreateAttachmentRequestSchema>;
export type TCreateAttachmentResponse = z.infer<typeof ZCreateAttachmentResponseSchema>;

View File

@ -0,0 +1,36 @@
import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
ZDeleteAttachmentResponseSchema,
} from './delete-attachment.types';
export const deleteAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/attachment/delete',
summary: 'Delete attachment',
description: 'Delete an attachment from a document',
tags: ['Document'],
},
})
.input(ZDeleteAttachmentRequestSchema)
.output(ZDeleteAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id } = input;
ctx.logger.info({
input: { id },
});
await deleteAttachment({
id,
userId,
teamId,
});
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
export const ZDeleteAttachmentResponseSchema = z.void();
export type TDeleteAttachmentRequest = z.infer<typeof ZDeleteAttachmentRequestSchema>;
export type TDeleteAttachmentResponse = z.infer<typeof ZDeleteAttachmentResponseSchema>;

View File

@ -0,0 +1,52 @@
import { EnvelopeType } from '@prisma/client';
import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { authenticatedProcedure } from '../../trpc';
import {
ZFindAttachmentsRequestSchema,
ZFindAttachmentsResponseSchema,
} from './find-attachments.types';
export const findAttachmentsRoute = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/attachment',
summary: 'Find attachments',
description: 'Find all attachments for a document',
tags: ['Document'],
},
})
.input(ZFindAttachmentsRequestSchema)
.output(ZFindAttachmentsResponseSchema)
.query(async ({ input, ctx }) => {
const { documentId } = input;
const { teamId } = ctx;
const userId = ctx.user.id;
ctx.logger.info({
input: { documentId },
});
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
userId,
teamId,
type: EnvelopeType.DOCUMENT,
});
const data = await findAttachmentsByEnvelopeId({
envelopeId: envelope.id,
teamId,
userId,
});
return {
data,
};
});

View File

@ -0,0 +1,21 @@
import { z } from 'zod';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
export const ZFindAttachmentsRequestSchema = z.object({
documentId: z.number(),
});
export const ZFindAttachmentsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
type: ZEnvelopeAttachmentTypeSchema,
label: z.string(),
data: z.string(),
}),
),
});
export type TFindAttachmentsRequest = z.infer<typeof ZFindAttachmentsRequestSchema>;
export type TFindAttachmentsResponse = z.infer<typeof ZFindAttachmentsResponseSchema>;

View File

@ -0,0 +1,37 @@
import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
ZUpdateAttachmentResponseSchema,
} from './update-attachment.types';
export const updateAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/attachment/update',
summary: 'Update attachment',
description: 'Update an existing attachment',
tags: ['Document'],
},
})
.input(ZUpdateAttachmentRequestSchema)
.output(ZUpdateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id, data } = input;
ctx.logger.info({
input: { id },
});
await updateAttachment({
id,
userId,
teamId,
data,
});
});

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZUpdateAttachmentRequestSchema = z.object({
id: z.string(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZUpdateAttachmentResponseSchema = z.void();
export type TUpdateAttachmentRequest = z.infer<typeof ZUpdateAttachmentRequestSchema>;
export type TUpdateAttachmentResponse = z.infer<typeof ZUpdateAttachmentResponseSchema>;

View File

@ -1,10 +1,12 @@
import { DocumentDataType } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
@ -35,6 +37,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
recipients,
meta,
folderId,
attachments,
} = input;
const { remaining } = await getServerLimits({ userId: user.id, teamId });
@ -55,27 +58,87 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
type: DocumentDataType.S3_PATH,
});
const createdDocument = await createDocumentV2({
const createdEnvelope = await createEnvelope({
userId: ctx.user.id,
teamId,
documentDataId: documentData.id,
normalizePdf: false, // Not normalizing because of presigned URL.
internalVersion: 1,
data: {
type: EnvelopeType.DOCUMENT,
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
recipients: (recipients || []).map((recipient) => ({
...recipient,
fields: (recipient.fields || []).map((field) => ({
...field,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
documentDataId: documentData.id,
})),
})),
folderId,
envelopeItems: [
{
// If you ever allow more than 1 in this endpoint, make sure to use `maximumEnvelopeItemCount` to limit it.
documentDataId: documentData.id,
},
],
},
attachments,
meta: {
...meta,
emailSettings: meta?.emailSettings ?? undefined,
},
meta,
requestMetadata: ctx.metadata,
});
const envelopeItems = await prisma.envelopeItem.findMany({
where: {
envelopeId: createdEnvelope.id,
},
include: {
documentData: true,
},
});
const legacyDocumentId = mapSecondaryIdToDocumentId(createdEnvelope.secondaryId);
const firstDocumentData = envelopeItems[0].documentData;
if (!firstDocumentData) {
throw new Error('Document data not found');
}
return {
document: createdDocument,
folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
document: {
...createdEnvelope,
envelopeId: createdEnvelope.id,
documentDataId: firstDocumentData.id,
documentData: {
...firstDocumentData,
envelopeItemId: envelopeItems[0].id,
},
documentMeta: {
...createdEnvelope.documentMeta,
documentId: legacyDocumentId,
},
id: legacyDocumentId,
fields: createdEnvelope.fields.map((field) => ({
...field,
documentId: legacyDocumentId,
templateId: null,
})),
recipients: createdEnvelope.recipients.map((recipient) => ({
...recipient,
documentId: legacyDocumentId,
templateId: null,
})),
},
folder: createdEnvelope.folder, // Todo: Remove this prior to api-v2 release.
uploadUrl: url,
};
});

View File

@ -1,4 +1,3 @@
import { DocumentSigningOrder } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentSchema } from '@documenso/lib/types/document';
@ -6,8 +5,9 @@ import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
@ -21,16 +21,6 @@ import { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from './schema';
@ -78,31 +68,18 @@ export const ZCreateDocumentTemporaryRequestSchema = z.object({
.optional(),
}),
)
.refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{ message: 'Recipients must have unique emails' },
.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
meta: ZDocumentMetaCreateSchema.optional(),
});
export const ZCreateDocumentTemporaryResponseSchema = z.object({

View File

@ -1,6 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { authenticatedProcedure } from '../trpc';
import {
@ -9,11 +12,11 @@ import {
} from './create-document.types';
export const createDocumentRoute = authenticatedProcedure
.input(ZCreateDocumentRequestSchema)
.input(ZCreateDocumentRequestSchema) // Note: Before releasing this to public, update the response schema to be correct.
.output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId } = input;
const { title, documentDataId, timezone, folderId, attachments } = input;
ctx.logger.info({
input: {
@ -30,18 +33,28 @@ export const createDocumentRoute = authenticatedProcedure
});
}
const document = await createDocument({
const document = await createEnvelope({
userId: user.id,
teamId,
title,
documentDataId,
internalVersion: 1,
data: {
type: EnvelopeType.DOCUMENT,
title,
userTimezone: timezone,
folderId,
envelopeItems: [
{
// If you ever allow more than 1 in this endpoint, make sure to use `maximumEnvelopeItemCount` to limit it.
documentDataId,
},
],
},
attachments,
normalizePdf: true,
userTimezone: timezone,
requestMetadata: ctx.metadata,
folderId,
});
return {
id: document.id,
legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId),
};
});

View File

@ -1,6 +1,9 @@
import { z } from 'zod';
import { ZDocumentMetaTimezoneSchema, ZDocumentTitleSchema } from './schema';
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { ZDocumentTitleSchema } from './schema';
// Currently not in use until we allow passthrough documents on create.
// export const createDocumentMeta: TrpcRouteMeta = {
@ -17,10 +20,19 @@ export const ZCreateDocumentRequestSchema = z.object({
documentDataId: z.string().min(1),
timezone: ZDocumentMetaTimezoneSchema.optional(),
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
export const ZCreateDocumentResponseSchema = z.object({
id: z.number(),
legacyDocumentId: z.number(),
});
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;

View File

@ -25,7 +25,10 @@ export const deleteDocumentRoute = authenticatedProcedure
const userId = ctx.user.id;
await deleteDocument({
id: documentId,
id: {
type: 'documentId',
id: documentId,
},
userId,
teamId,
requestMetadata: ctx.metadata,

View File

@ -1,5 +1,6 @@
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { mapEnvelopeToDocumentLite } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
import {
@ -23,17 +24,20 @@ export const distributeDocumentRoute = authenticatedProcedure
});
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
await updateDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
id: {
type: 'documentId',
id: documentId,
},
subject: meta.subject,
message: meta.message,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings,
emailSettings: meta.emailSettings ?? undefined,
language: meta.language,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
@ -41,10 +45,15 @@ export const distributeDocumentRoute = authenticatedProcedure
});
}
return await sendDocument({
const envelope = await sendDocument({
userId: ctx.user.id,
documentId,
id: {
type: 'documentId',
id: documentId,
},
teamId,
requestMetadata: ctx.metadata,
});
return mapEnvelopeToDocumentLite(envelope);
});

View File

@ -2,8 +2,6 @@ import { z } from 'zod';
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@ -12,7 +10,9 @@ import {
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
} from './schema';
} from '@documenso/lib/types/document-meta';
import type { TrpcRouteMeta } from '../trpc';
export const distributeDocumentMeta: TrpcRouteMeta = {
openapi: {

View File

@ -1,9 +1,11 @@
import { EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { authenticatedProcedure } from '../trpc';
import {
@ -24,20 +26,24 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure
},
});
const document = await getDocumentById({
documentId,
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
if (!envelope) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have access to this document.',
});
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});

View File

@ -1,10 +1,12 @@
import { EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { authenticatedProcedure } from '../trpc';
import {
@ -25,18 +27,22 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
},
});
const document = await getDocumentById({
documentId,
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId: ctx.user.id,
teamId,
});
if (!isDocumentCompleted(document.status)) {
if (!isDocumentCompleted(envelope.status)) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});

View File

@ -1,7 +1,8 @@
import { DocumentDataType } from '@prisma/client';
import type { DocumentData } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
@ -27,45 +28,51 @@ export const downloadDocumentRoute = authenticatedProcedure
},
});
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId: user.id,
teamId,
});
// This error is done AFTER the get envelope so we can test access controls without S3.
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document downloads are only available when S3 storage is configured.',
});
}
const document = await getDocumentById({
documentId,
userId: user.id,
teamId,
});
const documentData: DocumentData | undefined = envelope.envelopeItems[0]?.documentData;
if (!document.documentData) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document data not found',
if (envelope.envelopeItems.length !== 1 || !documentData) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'This endpoint only supports documents with a single item. Use envelopes API instead.',
});
}
if (document.documentData.type !== DocumentDataType.S3_PATH) {
if (documentData.type !== DocumentDataType.S3_PATH) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document is not stored in S3 and cannot be downloaded via URL.',
});
}
if (version === 'signed' && !isDocumentCompleted(document.status)) {
if (version === 'signed' && !isDocumentCompleted(envelope.status)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document is not completed yet.',
});
}
try {
const documentData =
version === 'original'
? document.documentData.initialData || document.documentData.data
: document.documentData.data;
const data =
version === 'original' ? documentData.initialData || documentData.data : documentData.data;
const { url } = await getPresignGetUrl(documentData);
const { url } = await getPresignGetUrl(data);
const baseTitle = document.title.replace(/\.pdf$/, '');
const baseTitle = envelope.title.replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;

View File

@ -1,4 +1,4 @@
import { duplicateDocument } from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { duplicateEnvelope } from '@documenso/lib/server-only/envelope/duplicate-envelope';
import { authenticatedProcedure } from '../trpc';
import {
@ -21,9 +21,17 @@ export const duplicateDocumentRoute = authenticatedProcedure
},
});
return await duplicateDocument({
const duplicatedEnvelope = await duplicateEnvelope({
id: {
type: 'documentId',
id: documentId,
},
userId: user.id,
teamId,
documentId,
});
return {
id: duplicatedEnvelope.id,
documentId: duplicatedEnvelope.legacyId.id,
};
});

View File

@ -16,7 +16,8 @@ export const ZDuplicateDocumentRequestSchema = z.object({
});
export const ZDuplicateDocumentResponseSchema = z.object({
documentId: z.number(),
id: z.string().describe('The envelope ID'),
documentId: z.number().describe('The legacy document ID'),
});
export type TDuplicateDocumentRequest = z.infer<typeof ZDuplicateDocumentRequestSchema>;

View File

@ -2,6 +2,7 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
import {
@ -69,6 +70,7 @@ export const findDocumentsInternalRoute = authenticatedProcedure
return {
...documents,
data: documents.data.map((envelope) => mapEnvelopesToDocumentMany(envelope)),
stats,
};
});

View File

@ -1,4 +1,5 @@
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
import {
@ -39,5 +40,8 @@ export const findDocumentsRoute = authenticatedProcedure
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
return documents;
return {
...documents,
data: documents.data.map((envelope) => mapEnvelopesToDocumentMany(envelope)),
};
});

View File

@ -1,7 +1,8 @@
import type { Document, Prisma } from '@prisma/client';
import { DocumentStatus, RecipientRole } from '@prisma/client';
import type { Envelope, Prisma } from '@prisma/client';
import { DocumentStatus, EnvelopeType, RecipientRole } from '@prisma/client';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document';
import { maskRecipientTokensForDocument } from '@documenso/lib/utils/mask-recipient-tokens-for-document';
import { prisma } from '@documenso/prisma';
@ -16,11 +17,16 @@ export const findInboxRoute = authenticatedProcedure
const userId = ctx.user.id;
return await findInbox({
const envelopes = await findInbox({
userId,
page,
perPage,
});
return {
...envelopes,
data: envelopes.data.map(mapEnvelopesToDocumentMany),
};
});
export type FindInboxOptions = {
@ -28,7 +34,7 @@ export type FindInboxOptions = {
page?: number;
perPage?: number;
orderBy?: {
column: keyof Omit<Document, 'document'>;
column: keyof Omit<Envelope, 'envelope'>;
direction: 'asc' | 'desc';
};
};
@ -38,12 +44,17 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
where: {
id: userId,
},
select: {
id: true,
email: true,
},
});
const orderByColumn = orderBy?.column ?? 'createdAt';
const orderByDirection = orderBy?.direction ?? 'desc';
const whereClause: Prisma.DocumentWhereInput = {
const whereClause: Prisma.EnvelopeWhereInput = {
type: EnvelopeType.DOCUMENT,
status: {
not: DocumentStatus.DRAFT,
},
@ -59,7 +70,7 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
};
const [data, count] = await Promise.all([
prisma.document.findMany({
prisma.envelope.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
@ -83,7 +94,7 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
},
},
}),
prisma.document.count({
prisma.envelope.count({
where: whereClause,
}),
]);

View File

@ -1,3 +1,5 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
@ -13,8 +15,9 @@ export const getDocumentByTokenRoute = authenticatedProcedure
.query(async ({ input, ctx }) => {
const { token } = input;
const document = await prisma.document.findFirst({
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.DOCUMENT,
recipients: {
some: {
token,
@ -23,21 +26,33 @@ export const getDocumentByTokenRoute = authenticatedProcedure
},
},
include: {
documentData: true,
envelopeItems: {
include: {
documentData: true,
},
},
},
});
if (!document) {
const firstDocumentData = envelope?.envelopeItems[0].documentData;
if (!envelope || !firstDocumentData) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
if (envelope.envelopeItems.length !== 1) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'This endpoint does not support multiple items',
});
}
ctx.logger.info({
documentId: document.id,
documentId: envelope.id,
});
return {
documentData: document.documentData,
documentData: firstDocumentData,
};
});

View File

@ -24,6 +24,9 @@ export const getDocumentRoute = authenticatedProcedure
return await getDocumentWithDetailsById({
userId: user.id,
teamId,
documentId,
id: {
type: 'documentId',
id: documentId,
},
});
});

View File

@ -1,4 +1,4 @@
import { DocumentStatus, RecipientRole } from '@prisma/client';
import { DocumentStatus, EnvelopeType, RecipientRole } from '@prisma/client';
import { prisma } from '@documenso/prisma';
@ -20,7 +20,8 @@ export const getInboxCountRoute = authenticatedProcedure
role: {
not: RecipientRole.CC,
},
document: {
envelope: {
type: EnvelopeType.DOCUMENT,
status: {
not: DocumentStatus.DRAFT,
},

View File

@ -26,7 +26,10 @@ export const redistributeDocumentRoute = authenticatedProcedure
await resendDocument({
userId: ctx.user.id,
teamId,
documentId,
id: {
type: 'documentId',
id: documentId,
},
recipients,
requestMetadata: ctx.metadata,
});

View File

@ -1,4 +1,9 @@
import { router } from '../trpc';
import { accessAuthRequest2FAEmailRoute } from './access-auth-request-2fa-email';
import { createAttachmentRoute } from './attachment/create-attachment';
import { deleteAttachmentRoute } from './attachment/delete-attachment';
import { findAttachmentsRoute } from './attachment/find-attachments';
import { updateAttachmentRoute } from './attachment/update-attachment';
import { createDocumentRoute } from './create-document';
import { createDocumentTemporaryRoute } from './create-document-temporary';
import { deleteDocumentRoute } from './delete-document';
@ -16,6 +21,7 @@ import { getDocumentByTokenRoute } from './get-document-by-token';
import { getInboxCountRoute } from './get-inbox-count';
import { redistributeDocumentRoute } from './redistribute-document';
import { searchDocumentRoute } from './search-document';
import { shareDocumentRoute } from './share-document';
import { updateDocumentRoute } from './update-document';
export const documentRouter = router({
@ -29,6 +35,7 @@ export const documentRouter = router({
distribute: distributeDocumentRoute,
redistribute: redistributeDocumentRoute,
search: searchDocumentRoute,
share: shareDocumentRoute,
// Temporary v2 beta routes to be removed once V2 is fully released.
download: downloadDocumentRoute,
@ -38,6 +45,10 @@ export const documentRouter = router({
getDocumentByToken: getDocumentByTokenRoute,
findDocumentsInternal: findDocumentsInternalRoute,
accessAuth: router({
request2FAEmail: accessAuthRequest2FAEmailRoute,
}),
auditLog: {
find: findDocumentAuditLogsRoute,
download: downloadDocumentAuditLogsRoute,
@ -46,4 +57,10 @@ export const documentRouter = router({
find: findInboxRoute,
getCount: getInboxCountRoute,
}),
attachment: {
create: createAttachmentRoute,
update: updateAttachmentRoute,
delete: deleteAttachmentRoute,
find: findAttachmentsRoute,
},
});

View File

@ -1,10 +1,6 @@
import { DocumentDistributionMethod, DocumentVisibility } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { VALID_DATE_FORMAT_VALUES } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
*
@ -34,58 +30,3 @@ export const ZDocumentExternalIdSchema = z
export const ZDocumentVisibilitySchema = z
.nativeEnum(DocumentVisibility)
.describe('The visibility of the document.');
export const ZDocumentMetaTimezoneSchema = z
.string()
.describe(
'The timezone to use for date fields and signing the document. Example Etc/UTC, Australia/Melbourne',
);
// Cooked.
// .refine((value) => TIME_ZONES.includes(value), {
// message: 'Invalid timezone. Please provide a valid timezone',
// });
export type TDocumentMetaTimezone = z.infer<typeof ZDocumentMetaTimezoneSchema>;
export const ZDocumentMetaDateFormatSchema = z
.enum(VALID_DATE_FORMAT_VALUES)
.describe('The date format to use for date fields and signing the document.');
export type TDocumentMetaDateFormat = z.infer<typeof ZDocumentMetaDateFormatSchema>;
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()
.max(254)
.describe('The subject of the email that will be sent to the recipients.');
export const ZDocumentMetaMessageSchema = z
.string()
.max(5000)
.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 recipients to sign using a typed signature.');
export const ZDocumentMetaDrawSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using a draw signature.');
export const ZDocumentMetaUploadSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using an uploaded signature.');

View File

@ -0,0 +1,28 @@
import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link';
import { procedure } from '../trpc';
import { ZShareDocumentRequestSchema, ZShareDocumentResponseSchema } from './share-document.types';
// Note: This is an unauthenticated route.
export const shareDocumentRoute = procedure
.input(ZShareDocumentRequestSchema)
.output(ZShareDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, token } = input;
ctx.logger.info({
input: {
documentId,
},
});
if (token) {
return await createOrGetShareLink({ documentId, token });
}
if (!ctx.user?.id) {
throw new Error('You must either provide a token or be logged in to create a sharing link.');
}
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
});

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
import DocumentShareLinkSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentShareLinkSchema';
export const ZShareDocumentRequestSchema = z.object({
documentId: z.number(),
token: z.string().optional(),
});
export const ZShareDocumentResponseSchema = DocumentShareLinkSchema.pick({
slug: true,
email: true,
});
export type TShareDocumentRequest = z.infer<typeof ZShareDocumentRequestSchema>;
export type TShareDocumentResponse = z.infer<typeof ZShareDocumentResponseSchema>;

View File

@ -1,18 +1,18 @@
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateDocumentRequestSchema,
ZUpdateDocumentResponseSchema,
} from './update-document.types';
import { updateDocumentMeta } from './update-document.types';
import { updateDocumentMeta as updateDocumentTrpcMeta } from './update-document.types';
/**
* Public route.
*/
export const updateDocumentRoute = authenticatedProcedure
.meta(updateDocumentMeta)
.meta(updateDocumentTrpcMeta)
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
@ -27,35 +27,23 @@ export const updateDocumentRoute = authenticatedProcedure
const userId = ctx.user.id;
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
allowDictateNextSigner: meta.allowDictateNextSigner,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
emailSettings: meta.emailSettings,
requestMetadata: ctx.metadata,
});
}
return await updateDocument({
const envelope = await updateEnvelope({
userId,
teamId,
documentId,
id: {
type: 'documentId',
id: documentId,
},
data,
meta,
requestMetadata: ctx.metadata,
});
const mappedDocument = {
...envelope,
id: mapSecondaryIdToDocumentId(envelope.secondaryId),
envelopeId: envelope.id,
};
return mappedDocument;
});

View File

@ -1,4 +1,3 @@
import { DocumentSigningOrder } from '@prisma/client';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
@ -7,21 +6,11 @@ import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from './schema';
@ -45,27 +34,10 @@ export const ZUpdateDocumentRequestSchema = z.object({
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
useLegacyFieldInsertion: z.boolean().optional(),
folderId: z.string().nullish(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
meta: ZDocumentMetaUpdateSchema.optional(),
});
export const ZUpdateDocumentResponseSchema = ZDocumentLiteSchema;

View File

@ -68,7 +68,7 @@ export const applyMultiSignSignatureRoute = procedure
const signatureFields = await prisma.field.findMany({
where: {
documentId: envelope.document.id,
envelopeId: envelope.document.id,
recipientId: envelope.recipient.id,
type: FieldType.SIGNATURE,
inserted: false,

View File

@ -1,6 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { procedure } from '../trpc';
import {
@ -8,6 +11,7 @@ import {
ZCreateEmbeddingDocumentResponseSchema,
} from './create-embedding-document.types';
// Todo: Envelopes - This only supports V1 documents/templates.
export const createEmbeddingDocumentRoute = procedure
.input(ZCreateEmbeddingDocumentRequestSchema)
.output(ZCreateEmbeddingDocumentResponseSchema)
@ -29,27 +33,44 @@ export const createEmbeddingDocumentRoute = procedure
const { title, documentDataId, externalId, recipients, meta } = input;
const document = await createDocumentV2({
const envelope = await createEnvelope({
internalVersion: 1,
data: {
type: EnvelopeType.DOCUMENT,
title,
externalId,
recipients,
recipients: (recipients || []).map((recipient) => ({
...recipient,
fields: (recipient.fields || []).map((field) => ({
...field,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
documentDataId,
})),
})),
envelopeItems: [
{
documentDataId,
},
],
},
meta,
documentDataId,
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
requestMetadata: metadata,
});
if (!document.id) {
if (!envelope.id) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create document: missing document ID',
});
}
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
return {
documentId: document.id,
documentId: legacyDocumentId,
};
} catch (error) {
if (error instanceof AppError) {

View File

@ -2,17 +2,6 @@ import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
@ -23,8 +12,18 @@ import {
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
} from '@documenso/lib/types/document-meta';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
export const ZCreateEmbeddingDocumentRequestSchema = z.object({
@ -47,14 +46,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
.optional(),
}),
)
.refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{ message: 'Recipients must have unique emails' },
)
.optional(),
meta: z
.object({

View File

@ -1,6 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
@ -9,10 +12,11 @@ import {
ZCreateEmbeddingTemplateResponseSchema,
} from './create-embedding-template.types';
// Todo: Envelopes - This only supports V1 documents/templates.
export const createEmbeddingTemplateRoute = procedure
.input(ZCreateEmbeddingTemplateRequestSchema)
.output(ZCreateEmbeddingTemplateResponseSchema)
.mutation(async ({ input, ctx: { req } }) => {
.mutation(async ({ input, ctx: { req, metadata } }) => {
try {
const authorizationHeader = req.headers.get('authorization');
@ -31,20 +35,30 @@ export const createEmbeddingTemplateRoute = procedure
const { title, documentDataId, recipients, meta } = input;
// First create the template
const template = await createTemplate({
const template = await createEnvelope({
internalVersion: 1,
userId: apiToken.userId,
data: {
title,
},
templateDocumentDataId: documentDataId,
teamId: apiToken.teamId ?? undefined,
data: {
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: [
{
documentDataId,
},
],
},
meta,
requestMetadata: metadata,
});
const firstEnvelopeItem = template.envelopeItems[0];
await Promise.all(
recipients.map(async (recipient) => {
const createdRecipient = await prisma.recipient.create({
data: {
templateId: template.id,
envelopeId: template.id,
email: recipient.email,
name: recipient.name || '',
role: recipient.role || 'SIGNER',
@ -57,6 +71,8 @@ export const createEmbeddingTemplateRoute = procedure
const createdFields = await prisma.field.createMany({
data: fields.map((field) => ({
envelopeId: template.id,
envelopeItemId: firstEnvelopeItem.id,
recipientId: createdRecipient.id,
type: field.type,
page: field.pageNumber,
@ -66,7 +82,6 @@ export const createEmbeddingTemplateRoute = procedure
height: field.height,
customText: '',
inserted: false,
templateId: template.id,
})),
});
@ -77,37 +92,6 @@ export const createEmbeddingTemplateRoute = procedure
}),
);
// Update the template meta if needed
if (meta) {
const upsertMetaData = {
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
redirectUrl: meta.redirectUrl,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled,
emailSettings: meta.emailSettings,
};
await prisma.templateMeta.upsert({
where: {
templateId: template.id,
},
create: {
templateId: template.id,
...upsertMetaData,
},
update: {
...upsertMetaData,
},
});
}
if (!template.id) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create template: missing template ID',
@ -115,7 +99,7 @@ export const createEmbeddingTemplateRoute = procedure
}
return {
templateId: template.id,
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
};
} catch (error) {
if (error instanceof AppError) {

View File

@ -2,15 +2,6 @@ import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@ -22,8 +13,17 @@ import {
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
} from '@documenso/lib/types/document-meta';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZDocumentTitleSchema } from '../document-router/schema';
const ZFieldSchema = z.object({
type: z.nativeEnum(FieldType),

View File

@ -42,7 +42,13 @@ export const getMultiSignDocumentRoute = procedure
// Transform fields to match our schema
const transformedFields = fields.map((field) => ({
...field,
recipient,
recipient: {
...recipient,
documentId: document.id,
templateId: null,
},
documentId: document.id,
templateId: null,
}));
return {

View File

@ -25,9 +25,7 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
subject: true,
message: true,
timezone: true,
password: true,
dateFormat: true,
documentId: true,
redirectUrl: true,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,

View File

@ -1,7 +1,6 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { nanoid } from '@documenso/lib/universal/id';
@ -39,24 +38,18 @@ export const updateEmbeddingDocumentRoute = procedure
const { documentId, title, externalId, recipients, meta } = input;
if (meta && Object.values(meta).length > 0) {
await upsertDocumentMeta({
documentId: documentId,
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
...meta,
requestMetadata: ctx.metadata,
});
}
await updateDocument({
await updateEnvelope({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
documentId: documentId,
id: {
type: 'documentId',
id: documentId,
},
data: {
title,
externalId,
},
meta,
requestMetadata: ctx.metadata,
});
@ -68,7 +61,10 @@ export const updateEmbeddingDocumentRoute = procedure
const { recipients: updatedRecipients } = await setDocumentRecipients({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
documentId: documentId,
id: {
type: 'documentId',
id: documentId,
},
recipients: recipientsWithClientId.map((recipient) => ({
id: recipient.id,
clientId: recipient.clientId,
@ -100,7 +96,10 @@ export const updateEmbeddingDocumentRoute = procedure
await setFieldsForDocument({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
documentId,
id: {
type: 'documentId',
id: documentId,
},
fields: fields.map((field) => ({
...field,
pageWidth: field.width,

View File

@ -2,17 +2,6 @@ import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
@ -23,43 +12,47 @@ import {
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
} from '@documenso/lib/types/document-meta';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types';
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from '../document-router/schema';
export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
documentId: z.number(),
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
recipients: z
.array(
z.object({
id: z.number().optional(),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
fields: ZFieldAndMetaSchema.and(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
)
.refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{ message: 'Recipients must have unique emails' },
),
recipients: z.array(
z.object({
id: z.number().optional(),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
envelopeItemId: z.string(),
}),
)
.array()
.optional(),
}),
),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),

View File

@ -1,8 +1,8 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
import { nanoid } from '@documenso/lib/universal/id';
import { procedure } from '../trpc';
@ -38,15 +38,19 @@ export const updateEmbeddingTemplateRoute = procedure
const { templateId, title, externalId, recipients, meta } = input;
await updateTemplate({
templateId,
await updateEnvelope({
id: {
type: 'templateId',
id: templateId,
},
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
teamId: apiToken.teamId,
data: {
title,
externalId,
},
meta,
requestMetadata: ctx.metadata,
});
const recipientsWithClientId = recipients.map((recipient) => ({
@ -57,7 +61,10 @@ export const updateEmbeddingTemplateRoute = procedure
const { recipients: updatedRecipients } = await setTemplateRecipients({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
templateId,
id: {
type: 'templateId',
id: templateId,
},
recipients: recipientsWithClientId.map((recipient) => ({
id: recipient.id,
email: recipient.email,
@ -87,7 +94,10 @@ export const updateEmbeddingTemplateRoute = procedure
await setFieldsForTemplate({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
templateId,
id: {
type: 'templateId',
id: templateId,
},
fields: fields.map((field) => ({
...field,
pageWidth: field.width,

View File

@ -2,15 +2,6 @@ import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
@ -22,8 +13,17 @@ import {
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
} from '../document-router/schema';
} from '@documenso/lib/types/document-meta';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZDocumentTitleSchema } from '../document-router/schema';
const ZFieldSchema = z.object({
id: z.number().optional(),
@ -34,6 +34,7 @@ const ZFieldSchema = z.object({
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
fieldMeta: ZFieldMetaSchema.optional(),
envelopeItemId: z.string(),
});
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({

View File

@ -0,0 +1,37 @@
import { createAttachment } from '@documenso/lib/server-only/envelope-attachment/create-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateAttachmentRequestSchema,
ZCreateAttachmentResponseSchema,
} from './create-attachment.types';
export const createAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/attachment/create',
summary: 'Create attachment',
description: 'Create a new attachment for an envelope',
tags: ['Envelope'],
},
})
.input(ZCreateAttachmentRequestSchema)
.output(ZCreateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { envelopeId, data } = input;
ctx.logger.info({
input: { envelopeId, label: data.label },
});
await createAttachment({
envelopeId,
teamId,
userId,
data,
});
});

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZCreateAttachmentRequestSchema = z.object({
envelopeId: z.string(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZCreateAttachmentResponseSchema = z.void();
export type TCreateAttachmentRequest = z.infer<typeof ZCreateAttachmentRequestSchema>;
export type TCreateAttachmentResponse = z.infer<typeof ZCreateAttachmentResponseSchema>;

View File

@ -0,0 +1,36 @@
import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
ZDeleteAttachmentResponseSchema,
} from './delete-attachment.types';
export const deleteAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/attachment/delete',
summary: 'Delete attachment',
description: 'Delete an attachment from an envelope',
tags: ['Envelope'],
},
})
.input(ZDeleteAttachmentRequestSchema)
.output(ZDeleteAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id } = input;
ctx.logger.info({
input: { id },
});
await deleteAttachment({
id,
userId,
teamId,
});
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
export const ZDeleteAttachmentResponseSchema = z.void();
export type TDeleteAttachmentRequest = z.infer<typeof ZDeleteAttachmentRequestSchema>;
export type TDeleteAttachmentResponse = z.infer<typeof ZDeleteAttachmentResponseSchema>;

View File

@ -0,0 +1,52 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id';
import { findAttachmentsByToken } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-token';
import { procedure } from '../../trpc';
import {
ZFindAttachmentsRequestSchema,
ZFindAttachmentsResponseSchema,
} from './find-attachments.types';
export const findAttachmentsRoute = procedure
.meta({
openapi: {
method: 'GET',
path: '/envelope/attachment',
summary: 'Find attachments',
description: 'Find all attachments for an envelope',
tags: ['Envelope'],
},
})
.input(ZFindAttachmentsRequestSchema)
.output(ZFindAttachmentsResponseSchema)
.query(async ({ input, ctx }) => {
const { envelopeId, token } = input;
ctx.logger.info({
input: { envelopeId },
});
if (token) {
const data = await findAttachmentsByToken({ envelopeId, token });
return {
data,
};
}
const { teamId } = ctx;
const userId = ctx.user?.id;
if (!userId || !teamId) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You must be authenticated to access this resource',
});
}
const data = await findAttachmentsByEnvelopeId({ envelopeId, teamId, userId });
return {
data,
};
});

View File

@ -0,0 +1,22 @@
import { z } from 'zod';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
export const ZFindAttachmentsRequestSchema = z.object({
envelopeId: z.string(),
token: z.string().optional(),
});
export const ZFindAttachmentsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
type: ZEnvelopeAttachmentTypeSchema,
label: z.string(),
data: z.string(),
}),
),
});
export type TFindAttachmentsRequest = z.infer<typeof ZFindAttachmentsRequestSchema>;
export type TFindAttachmentsResponse = z.infer<typeof ZFindAttachmentsResponseSchema>;

View File

@ -0,0 +1,37 @@
import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
ZUpdateAttachmentResponseSchema,
} from './update-attachment.types';
export const updateAttachmentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/attachment/update',
summary: 'Update attachment',
description: 'Update an existing attachment',
tags: ['Envelope'],
},
})
.input(ZUpdateAttachmentRequestSchema)
.output(ZUpdateAttachmentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const userId = ctx.user.id;
const { id, data } = input;
ctx.logger.info({
input: { id },
});
await updateAttachment({
id,
userId,
teamId,
data,
});
});

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
export const ZUpdateAttachmentRequestSchema = z.object({
id: z.string(),
data: z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
}),
});
export const ZUpdateAttachmentResponseSchema = z.void();
export type TUpdateAttachmentRequest = z.infer<typeof ZUpdateAttachmentRequestSchema>;
export type TUpdateAttachmentResponse = z.infer<typeof ZUpdateAttachmentResponseSchema>;

View File

@ -0,0 +1,153 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { prefixedId } from '@documenso/lib/universal/id';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateEnvelopeItemsRequestSchema,
ZCreateEnvelopeItemsResponseSchema,
} from './create-envelope-items.types';
export const createEnvelopeItemsRoute = authenticatedProcedure
.input(ZCreateEnvelopeItemsRequestSchema)
.output(ZCreateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { envelopeId, items } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
userId: user.id,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: true,
envelopeItems: {
orderBy: {
order: 'asc',
},
},
team: {
select: {
organisation: {
select: {
organisationClaim: true,
},
},
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
});
}
const organisationClaim = envelope.team.organisation.organisationClaim;
const remainingEnvelopeItems =
organisationClaim.envelopeItemCount - envelope.envelopeItems.length - items.length;
if (remainingEnvelopeItems < 0) {
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
message: `You cannot upload more than ${organisationClaim.envelopeItemCount} envelope items`,
statusCode: 400,
});
}
const foundDocumentData = await prisma.documentData.findMany({
where: {
id: {
in: items.map((item) => item.documentDataId),
},
},
select: {
envelopeItem: {
select: {
id: true,
},
},
},
});
// Check that all the document data was found.
if (foundDocumentData.length !== items.length) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document data not found',
});
}
// Check that it doesn't already have an envelope item.
if (foundDocumentData.some((documentData) => documentData.envelopeItem?.id)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document data not found',
});
}
const currentHighestOrderValue =
envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1;
const result = await prisma.$transaction(async (tx) => {
const createdItems = await tx.envelopeItem.createManyAndReturn({
data: items.map((item) => ({
id: prefixedId('envelope_item'),
envelopeId,
title: item.title,
documentDataId: item.documentDataId,
order: currentHighestOrderValue + 1,
})),
include: {
documentData: true,
},
});
await tx.documentAuditLog.createMany({
data: createdItems.map((item) =>
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED,
envelopeId: envelope.id,
data: {
envelopeItemId: item.id,
envelopeItemTitle: item.title,
},
user: {
name: user.name,
email: user.email,
},
requestMetadata: metadata.requestMetadata,
}),
),
});
return createdItems;
});
return {
createdEnvelopeItems: result,
};
});

View File

@ -0,0 +1,38 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { ZDocumentTitleSchema } from '../document-router/schema';
export const ZCreateEnvelopeItemsRequestSchema = z.object({
envelopeId: z.string(),
items: z
.object({
title: ZDocumentTitleSchema,
documentDataId: z.string(),
})
.array(),
});
export const ZCreateEnvelopeItemsResponseSchema = z.object({
createdEnvelopeItems: EnvelopeItemSchema.pick({
id: true,
title: true,
documentDataId: true,
envelopeId: true,
order: true,
})
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
});
export type TCreateEnvelopeItemsRequest = z.infer<typeof ZCreateEnvelopeItemsRequestSchema>;
export type TCreateEnvelopeItemsResponse = z.infer<typeof ZCreateEnvelopeItemsResponseSchema>;

View File

@ -0,0 +1,79 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateEnvelopeRequestSchema,
ZCreateEnvelopeResponseSchema,
} from './create-envelope.types';
export const createEnvelopeRoute = authenticatedProcedure
.input(ZCreateEnvelopeRequestSchema)
.output(ZCreateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const {
title,
type,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
folderId,
items,
meta,
attachments,
} = input;
ctx.logger.info({
input: {
folderId,
},
});
const { remaining, maximumEnvelopeItemCount } = await getServerLimits({
userId: user.id,
teamId,
});
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
if (items.length > maximumEnvelopeItemCount) {
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
statusCode: 400,
});
}
const envelope = await createEnvelope({
userId: user.id,
teamId,
internalVersion: 2,
data: {
type,
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
folderId,
envelopeItems: items,
},
attachments,
meta,
normalizePdf: true,
requestMetadata: ctx.metadata,
});
return {
id: envelope.id,
};
});

View File

@ -0,0 +1,96 @@
import { EnvelopeType } from '@prisma/client';
import { z } from 'zod';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZDocumentExternalIdSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
// Currently not in use until we allow passthrough documents on create.
// export const createEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/create',
// summary: 'Create envelope',
// tags: ['Envelope'],
// },
// };
export const ZCreateEnvelopeRequestSchema = z.object({
title: ZDocumentTitleSchema,
type: z.nativeEnum(EnvelopeType),
externalId: ZDocumentExternalIdSchema.optional(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(),
items: z
.object({
title: ZDocumentTitleSchema.optional(),
documentDataId: z.string(),
})
.array(),
folderId: z
.string()
.describe(
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
)
.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
documentDataId: z
.string()
.describe(
'The ID of the document data to create the field on. If empty, the first document data will be used.',
),
page: ZFieldPageNumberSchema,
positionX: ZFieldPageXSchema,
positionY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
)
.optional(),
meta: ZDocumentMetaCreateSchema.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
});
export const ZCreateEnvelopeResponseSchema = z.object({
id: z.string(),
});
export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>;
export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>;

View File

@ -0,0 +1,101 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteEnvelopeItemRequestSchema,
ZDeleteEnvelopeItemResponseSchema,
} from './delete-envelope-item.types';
export const deleteEnvelopeItemRoute = authenticatedProcedure
.input(ZDeleteEnvelopeItemRequestSchema)
.output(ZDeleteEnvelopeItemResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { envelopeId, envelopeItemId } = input;
ctx.logger.info({
input: {
envelopeId,
envelopeItemId,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
userId: user.id,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
});
}
const result = await prisma.$transaction(async (tx) => {
const deletedEnvelopeItem = await tx.envelopeItem.delete({
where: {
id: envelopeItemId,
envelopeId: envelope.id,
},
select: {
id: true,
title: true,
documentData: {
select: {
id: true,
},
},
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED,
envelopeId: envelope.id,
data: {
envelopeItemId: deletedEnvelopeItem.id,
envelopeItemTitle: deletedEnvelopeItem.title,
},
user: {
name: user.name,
email: user.email,
},
requestMetadata: metadata.requestMetadata,
}),
});
return deletedEnvelopeItem;
});
await prisma.documentData.delete({
where: {
id: result.documentData.id,
envelopeItem: {
is: null,
},
},
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZDeleteEnvelopeItemRequestSchema = z.object({
envelopeId: z.string(),
envelopeItemId: z.string(),
});
export const ZDeleteEnvelopeItemResponseSchema = z.void();
export type TDeleteEnvelopeItemRequest = z.infer<typeof ZDeleteEnvelopeItemRequestSchema>;
export type TDeleteEnvelopeItemResponse = z.infer<typeof ZDeleteEnvelopeItemResponseSchema>;

View File

@ -0,0 +1,50 @@
import { EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteEnvelopeRequestSchema,
ZDeleteEnvelopeResponseSchema,
} from './delete-envelope.types';
export const deleteEnvelopeRoute = authenticatedProcedure
// .meta(deleteEnvelopeMeta)
.input(ZDeleteEnvelopeRequestSchema)
.output(ZDeleteEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, envelopeType } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
await match(envelopeType)
.with(EnvelopeType.DOCUMENT, async () =>
deleteDocument({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
requestMetadata: ctx.metadata,
}),
)
.with(EnvelopeType.TEMPLATE, async () =>
deleteTemplate({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
}),
)
.exhaustive();
});

View File

@ -0,0 +1,21 @@
import { EnvelopeType } from '@prisma/client';
import { z } from 'zod';
// export const deleteEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/delete',
// summary: 'Delete envelope',
// tags: ['Envelope'],
// },
// };
export const ZDeleteEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
});
export const ZDeleteEnvelopeResponseSchema = z.void();
export type TDeleteEnvelopeRequest = z.infer<typeof ZDeleteEnvelopeRequestSchema>;
export type TDeleteEnvelopeResponse = z.infer<typeof ZDeleteEnvelopeResponseSchema>;

View File

@ -0,0 +1,55 @@
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { authenticatedProcedure } from '../trpc';
import {
ZDistributeEnvelopeRequestSchema,
ZDistributeEnvelopeResponseSchema,
} from './distribute-envelope.types';
export const distributeEnvelopeRoute = authenticatedProcedure
// .meta(distributeEnvelopeMeta)
.input(ZDistributeEnvelopeRequestSchema)
.output(ZDistributeEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, meta = {} } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
if (Object.values(meta).length > 0) {
await updateDocumentMeta({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
subject: meta.subject,
message: meta.message,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings ?? undefined,
language: meta.language,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
requestMetadata: ctx.metadata,
});
}
await sendDocument({
userId: ctx.user.id,
id: {
type: 'envelopeId',
id: envelopeId,
},
teamId,
requestMetadata: ctx.metadata,
});
});

View File

@ -0,0 +1,34 @@
import { z } from 'zod';
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
// export const distributeEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/distribute',
// summary: 'Distribute envelope',
// description: 'Send the document out to recipients based on your distribution method',
// tags: ['Envelope'],
// },
// };
export const ZDistributeEnvelopeRequestSchema = z.object({
envelopeId: z.string().describe('The ID of the envelope to send.'),
meta: ZDocumentMetaUpdateSchema.pick({
subject: true,
message: true,
timezone: true,
dateFormat: true,
distributionMethod: true,
redirectUrl: true,
language: true,
emailId: true,
emailReplyTo: true,
emailSettings: true,
}).optional(),
});
export const ZDistributeEnvelopeResponseSchema = z.void();
export type TDistributeEnvelopeRequest = z.infer<typeof ZDistributeEnvelopeRequestSchema>;
export type TDistributeEnvelopeResponse = z.infer<typeof ZDistributeEnvelopeResponseSchema>;

View File

@ -0,0 +1,34 @@
import { duplicateEnvelope } from '@documenso/lib/server-only/envelope/duplicate-envelope';
import { authenticatedProcedure } from '../trpc';
import {
ZDuplicateEnvelopeRequestSchema,
ZDuplicateEnvelopeResponseSchema,
} from './duplicate-envelope.types';
export const duplicateEnvelopeRoute = authenticatedProcedure
.input(ZDuplicateEnvelopeRequestSchema)
.output(ZDuplicateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const duplicatedEnvelope = await duplicateEnvelope({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
});
return {
duplicatedEnvelopeId: duplicatedEnvelope.id,
};
});

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const ZDuplicateEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
});
export const ZDuplicateEnvelopeResponseSchema = z.object({
duplicatedEnvelopeId: z.string(),
});
export type TDuplicateEnvelopeRequest = z.infer<typeof ZDuplicateEnvelopeRequestSchema>;
export type TDuplicateEnvelopeResponse = z.infer<typeof ZDuplicateEnvelopeResponseSchema>;

View File

@ -0,0 +1,119 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { prisma } from '@documenso/prisma';
import { maybeAuthenticatedProcedure } from '../trpc';
import {
ZGetEnvelopeItemsByTokenRequestSchema,
ZGetEnvelopeItemsByTokenResponseSchema,
} from './get-envelope-items-by-token.types';
// Not intended for V2 API usage.
// NOTE: THIS IS A PUBLIC PROCEDURE
export const getEnvelopeItemsByTokenRoute = maybeAuthenticatedProcedure
.input(ZGetEnvelopeItemsByTokenRequestSchema)
.output(ZGetEnvelopeItemsByTokenResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeId, access } = input;
ctx.logger.info({
input: {
envelopeId,
access,
},
});
if (access.type === 'user') {
if (!user || !teamId) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'User not found',
});
}
return await handleGetEnvelopeItemsByUser({ envelopeId, userId: user.id, teamId });
}
return await handleGetEnvelopeItemsByToken({ envelopeId, token: access.token });
});
const handleGetEnvelopeItemsByToken = async ({
envelopeId,
token,
}: {
envelopeId: string;
token: string;
}) => {
const envelope = await prisma.envelope.findFirst({
where: {
id: envelopeId,
type: EnvelopeType.DOCUMENT, // You cannot get template envelope items by token.
recipients: {
some: {
token,
},
},
},
include: {
envelopeItems: {
include: {
documentData: true,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope could not be found',
});
}
return {
envelopeItems: envelope.envelopeItems,
};
};
const handleGetEnvelopeItemsByUser = async ({
envelopeId,
userId,
teamId,
}: {
envelopeId: string;
userId: number;
teamId: number;
}) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
userId,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
envelopeItems: {
include: {
documentData: true,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope could not be found',
});
}
return {
envelopeItems: envelope.envelopeItems,
};
};

View File

@ -0,0 +1,34 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
envelopeId: z.string(),
access: z.discriminatedUnion('type', [
z.object({
type: z.literal('recipient'),
token: z.string(),
}),
z.object({
type: z.literal('user'),
}),
]),
});
export const ZGetEnvelopeItemsByTokenResponseSchema = z.object({
envelopeItems: EnvelopeItemSchema.pick({
id: true,
title: true,
order: true,
})
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
});

View File

@ -0,0 +1,55 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetEnvelopeItemsRequestSchema,
ZGetEnvelopeItemsResponseSchema,
} from './get-envelope-items.types';
// Not intended for V2 API usage.
export const getEnvelopeItemsRoute = authenticatedProcedure
.input(ZGetEnvelopeItemsRequestSchema)
.output(ZGetEnvelopeItemsResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeId } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
userId: user.id,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
envelopeItems: {
include: {
documentData: true,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope could not be found',
});
}
return {
envelopeItems: envelope.envelopeItems,
};
});

View File

@ -0,0 +1,25 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
export const ZGetEnvelopeItemsRequestSchema = z.object({
envelopeId: z.string(),
});
export const ZGetEnvelopeItemsResponseSchema = z.object({
envelopeItems: EnvelopeItemSchema.pick({
id: true,
title: true,
order: true,
})
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
});

View File

@ -0,0 +1,29 @@
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { authenticatedProcedure } from '../trpc';
import { ZGetEnvelopeRequestSchema, ZGetEnvelopeResponseSchema } from './get-envelope.types';
export const getEnvelopeRoute = authenticatedProcedure
// .meta(getEnvelopeMeta)
.input(ZGetEnvelopeRequestSchema)
.output(ZGetEnvelopeResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeId } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
return await getEnvelopeById({
userId: user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
});
});

View File

@ -0,0 +1,22 @@
import { z } from 'zod';
import { ZEnvelopeSchema } from '@documenso/lib/types/envelope';
// export const getEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'GET',
// path: '/envelope/{envelopeId}',
// summary: 'Get envelope',
// description: 'Returns a envelope given an ID',
// tags: ['Envelope'],
// },
// };
export const ZGetEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
});
export const ZGetEnvelopeResponseSchema = ZEnvelopeSchema;
export type TGetEnvelopeRequest = z.infer<typeof ZGetEnvelopeRequestSchema>;
export type TGetEnvelopeResponse = z.infer<typeof ZGetEnvelopeResponseSchema>;

View File

@ -0,0 +1,34 @@
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { authenticatedProcedure } from '../trpc';
import {
ZRedistributeEnvelopeRequestSchema,
ZRedistributeEnvelopeResponseSchema,
} from './redistribute-envelope.types';
export const redistributeEnvelopeRoute = authenticatedProcedure
// .meta(redistributeEnvelopeMeta)
.input(ZRedistributeEnvelopeRequestSchema)
.output(ZRedistributeEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, recipients } = input;
ctx.logger.info({
input: {
envelopeId,
recipients,
},
});
await resendDocument({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
recipients,
requestMetadata: ctx.metadata,
});
});

View File

@ -0,0 +1,25 @@
import { z } from 'zod';
// export const redistributeEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/redistribute',
// summary: 'Redistribute document',
// description:
// 'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
// tags: ['Envelope'],
// },
// };
export const ZRedistributeEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
recipients: z
.array(z.number())
.min(1)
.describe('The IDs of the recipients to redistribute the envelope to.'),
});
export const ZRedistributeEnvelopeResponseSchema = z.void();
export type TRedistributeEnvelopeRequest = z.infer<typeof ZRedistributeEnvelopeRequestSchema>;
export type TRedistributeEnvelopeResponse = z.infer<typeof ZRedistributeEnvelopeResponseSchema>;

View File

@ -0,0 +1,52 @@
import { router } from '../trpc';
import { createAttachmentRoute } from './attachment/create-attachment';
import { deleteAttachmentRoute } from './attachment/delete-attachment';
import { findAttachmentsRoute } from './attachment/find-attachments';
import { updateAttachmentRoute } from './attachment/update-attachment';
import { createEnvelopeRoute } from './create-envelope';
import { createEnvelopeItemsRoute } from './create-envelope-items';
import { deleteEnvelopeRoute } from './delete-envelope';
import { deleteEnvelopeItemRoute } from './delete-envelope-item';
import { distributeEnvelopeRoute } from './distribute-envelope';
import { duplicateEnvelopeRoute } from './duplicate-envelope';
import { getEnvelopeRoute } from './get-envelope';
import { getEnvelopeItemsRoute } from './get-envelope-items';
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
import { redistributeEnvelopeRoute } from './redistribute-envelope';
import { setEnvelopeFieldsRoute } from './set-envelope-fields';
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
import { signEnvelopeFieldRoute } from './sign-envelope-field';
import { updateEnvelopeRoute } from './update-envelope';
import { updateEnvelopeItemsRoute } from './update-envelope-items';
export const envelopeRouter = router({
get: getEnvelopeRoute,
create: createEnvelopeRoute,
update: updateEnvelopeRoute,
delete: deleteEnvelopeRoute,
duplicate: duplicateEnvelopeRoute,
distribute: distributeEnvelopeRoute,
redistribute: redistributeEnvelopeRoute,
// share: shareEnvelopeRoute,
item: {
getMany: getEnvelopeItemsRoute,
getManyByToken: getEnvelopeItemsByTokenRoute,
createMany: createEnvelopeItemsRoute,
updateMany: updateEnvelopeItemsRoute,
delete: deleteEnvelopeItemRoute,
},
recipient: {
set: setEnvelopeRecipientsRoute,
},
field: {
set: setEnvelopeFieldsRoute,
sign: signEnvelopeFieldRoute,
},
attachment: {
find: findAttachmentsRoute,
create: createAttachmentRoute,
update: updateAttachmentRoute,
delete: deleteAttachmentRoute,
},
});

View File

@ -0,0 +1,73 @@
import { EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { authenticatedProcedure } from '../trpc';
import {
ZSetEnvelopeFieldsRequestSchema,
ZSetEnvelopeFieldsResponseSchema,
} from './set-envelope-fields.types';
// Note: This is intended to always be an internal route.
export const setEnvelopeFieldsRoute = authenticatedProcedure
.input(ZSetEnvelopeFieldsRequestSchema)
.output(ZSetEnvelopeFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, envelopeType, fields } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const result = await match(envelopeType)
.with(EnvelopeType.DOCUMENT, async () =>
setFieldsForDocument({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
fields: fields.map((field) => ({
...field,
pageNumber: field.page,
pageX: field.positionX,
pageY: field.positionY,
pageWidth: field.width,
pageHeight: field.height,
})),
requestMetadata: ctx.metadata,
}),
)
.with(EnvelopeType.TEMPLATE, async () =>
setFieldsForTemplate({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
fields: fields.map((field) => ({
...field,
pageNumber: field.page,
pageX: field.positionX,
pageY: field.positionY,
pageWidth: field.width,
pageHeight: field.height,
})),
}),
)
.exhaustive();
return {
fields: result.fields.map((field) => ({
id: field.id,
formId: field.formId,
})),
};
});

View File

@ -0,0 +1,59 @@
import { EnvelopeType, FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
export const ZSetEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
fields: z.array(
z.object({
id: z
.number()
.optional()
.describe('The id of the field. If not provided, a new field will be created.'),
formId: z.string().optional().describe('A temporary ID to keep track of new fields created'),
envelopeItemId: z.string().describe('The id of the envelope item to put the field on'),
recipientId: z.number(),
type: z.nativeEnum(FieldType),
page: z
.number()
.min(1)
.describe('The page number of the field on the envelope. Starts from 1.'),
// Todo: Envelopes - Extract these 0-100 schemas with better descriptions.
positionX: z
.number()
.min(0)
.max(100)
.describe('The percentage based X position of the field on the envelope.'),
positionY: z
.number()
.min(0)
.max(100)
.describe('The percentage based Y position of the field on the envelope.'),
width: z
.number()
.min(0)
.max(100)
.describe('The percentage based width of the field on the envelope.'),
height: z
.number()
.min(0)
.max(100)
.describe('The percentage based height of the field on the envelope.'),
fieldMeta: ZFieldMetaSchema, // Todo: Envelopes - Use a more strict form?
}),
),
});
export const ZSetEnvelopeFieldsResponseSchema = z.object({
fields: z
.object({
id: z.number(),
formId: z.string().optional(),
})
.array(),
});
export type TSetEnvelopeFieldsRequest = z.infer<typeof ZSetEnvelopeFieldsRequestSchema>;
export type TSetEnvelopeFieldsResponse = z.infer<typeof ZSetEnvelopeFieldsResponseSchema>;

View File

@ -0,0 +1,51 @@
import { EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { authenticatedProcedure } from '../trpc';
import {
ZSetEnvelopeRecipientsRequestSchema,
ZSetEnvelopeRecipientsResponseSchema,
} from './set-envelope-recipients.types';
export const setEnvelopeRecipientsRoute = authenticatedProcedure
.input(ZSetEnvelopeRecipientsRequestSchema)
.output(ZSetEnvelopeRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, envelopeType, recipients } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
return await match(envelopeType)
.with(EnvelopeType.DOCUMENT, async () =>
setDocumentRecipients({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
recipients,
requestMetadata: ctx.metadata,
}),
)
.with(EnvelopeType.TEMPLATE, async () =>
setTemplateRecipients({
userId: ctx.user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
recipients,
}),
)
.exhaustive();
});

View File

@ -0,0 +1,30 @@
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
recipients: z.array(
z.object({
id: z.number().optional(),
email: z.string().toLowerCase().email().min(1).max(254),
name: z.string().max(255),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
}),
),
});
export const ZSetEnvelopeRecipientsResponseSchema = z.object({
recipients: ZRecipientLiteSchema.omit({
documentId: true,
templateId: true,
}).array(),
});
export type TSetEnvelopeRecipientsRequest = z.infer<typeof ZSetEnvelopeRecipientsRequestSchema>;
export type TSetEnvelopeRecipientsResponse = z.infer<typeof ZSetEnvelopeRecipientsResponseSchema>;

View File

@ -0,0 +1,259 @@
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { match } from 'ts-pattern';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { validateFieldAuth } from '@documenso/lib/server-only/document/validate-field-auth';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
import {
ZSignEnvelopeFieldRequestSchema,
ZSignEnvelopeFieldResponseSchema,
} from './sign-envelope-field.types';
// Note that this is an unauthenticated public procedure route.
export const signEnvelopeFieldRoute = procedure
.input(ZSignEnvelopeFieldRequestSchema)
.output(ZSignEnvelopeFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, metadata } = ctx;
const { token, fieldId, fieldValue, authOptions } = input;
ctx.logger.info({
input: {
fieldId,
},
});
const recipient = await prisma.recipient.findFirst({
where: {
token,
},
});
if (!recipient) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const field = await prisma.field.findFirst({
where: {
id: fieldId,
recipient: {
...(recipient.role === RecipientRole.ASSISTANT
? {
signingStatus: {
not: SigningStatus.SIGNED,
},
signingOrder: {
gte: recipient.signingOrder ?? 0,
},
}
: {
id: recipient.id,
}),
},
},
include: {
envelope: {
include: {
recipients: true,
documentMeta: true,
},
},
recipient: true,
},
});
if (!field) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Field ${fieldId} not found`,
});
}
const { envelope } = field;
const { documentMeta } = envelope;
if (envelope.internalVersion !== 2) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Envelope ${envelope.id} is not a version 2 envelope`,
});
}
if (
field.type === FieldType.SIGNATURE &&
recipient.id !== field.recipientId &&
recipient.role === RecipientRole.ASSISTANT
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Assistant recipients cannot sign signature fields`,
});
}
if (fieldValue.type !== field.type) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Selected values do not match the field values',
});
}
if (envelope.deletedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Document ${envelope.id} has been deleted`,
});
}
if (envelope.status !== DocumentStatus.PENDING) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Document ${envelope.id} must be pending for signing`,
});
}
if (
recipient.signingStatus === SigningStatus.SIGNED ||
field.recipient.signingStatus === SigningStatus.SIGNED
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient ${recipient.id} has already signed`,
});
}
if (field.fieldMeta?.readOnly) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Field ${fieldId} is read only`,
});
}
// Unreachable code based on the above query but we need to satisfy TypeScript
if (field.recipientId === null) {
throw new Error(`Field ${fieldId} has no recipientId`);
}
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: envelope.authOptions,
recipient,
field,
userId: user?.id,
authOptions,
});
const assistant = recipient.role === RecipientRole.ASSISTANT ? recipient : undefined;
let signatureImageAsBase64 = null;
let typedSignature = null;
if (field.type === FieldType.SIGNATURE) {
if (fieldValue.type !== FieldType.SIGNATURE) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Field ${fieldId} is not a signature field`,
});
}
if (fieldValue.value) {
const isBase64 = isBase64Image(fieldValue.value);
signatureImageAsBase64 = isBase64 ? fieldValue.value : null;
typedSignature = !isBase64 ? fieldValue.value : null;
}
}
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: field.id,
},
data: {
customText: insertionValues.customText,
inserted: insertionValues.inserted,
},
include: {
signature: true,
},
});
if (field.type === FieldType.SIGNATURE) {
const signature = await tx.signature.upsert({
where: {
fieldId: field.id,
},
create: {
fieldId: field.id,
recipientId: field.recipientId,
signatureImageAsBase64: signatureImageAsBase64,
typedSignature: typedSignature,
},
update: {
signatureImageAsBase64: signatureImageAsBase64,
typedSignature: typedSignature,
},
});
// Dirty but I don't want to deal with type information
Object.assign(updatedField, {
signature,
});
}
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type:
assistant && field.recipientId !== assistant.id
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
envelopeId: envelope.id,
user: {
email: assistant?.email ?? recipient.email,
name: assistant?.name ?? recipient.name,
},
requestMetadata: metadata.requestMetadata,
data: {
recipientEmail: recipient.email,
recipientId: recipient.id,
recipientName: recipient.name,
recipientRole: recipient.role,
fieldId: updatedField.secondaryId,
field: match(updatedField.type)
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, (type) => ({
type,
data: signatureImageAsBase64 || typedSignature || '',
}))
.with(
FieldType.DATE,
FieldType.EMAIL,
FieldType.NAME,
FieldType.TEXT,
FieldType.INITIALS,
(type) => ({
type,
data: updatedField.customText,
}),
)
.with(
FieldType.NUMBER,
FieldType.RADIO,
FieldType.CHECKBOX,
FieldType.DROPDOWN,
(type) => ({
type,
data: updatedField.customText,
}),
)
.exhaustive(),
fieldSecurity: derivedRecipientActionAuth
? {
type: derivedRecipientActionAuth,
}
: undefined,
},
}),
});
return {
signedField: updatedField,
};
});
});

View File

@ -0,0 +1,69 @@
import { z } from 'zod';
import { ZRecipientActionAuthSchema } from '@documenso/lib/types/document-auth';
import { ZFieldSchema } from '@documenso/lib/types/field';
import { FieldType } from '@documenso/prisma/client';
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
export const ZSignEnvelopeFieldValue = z.discriminatedUnion('type', [
z.object({
type: z.literal(FieldType.CHECKBOX),
value: z.array(z.number()).describe('The indices of the selected options'),
}),
z.object({
type: z.literal(FieldType.RADIO),
value: z.number().nullable().describe('The index of the selected option'),
}),
z.object({
type: z.literal(FieldType.NUMBER),
value: z.number().nullable(),
}),
z.object({
type: z.literal(FieldType.EMAIL),
value: z.string().nullable(),
}),
z.object({
type: z.literal(FieldType.NAME),
value: z.string().nullable(),
}),
z.object({
type: z.literal(FieldType.INITIALS),
value: z.string().nullable(),
}),
z.object({
type: z.literal(FieldType.TEXT),
value: z.string().nullable(),
}),
z.object({
type: z.literal(FieldType.DROPDOWN),
value: z.string().nullable(),
}),
z.object({
type: z.literal(FieldType.DATE),
value: z.boolean(),
}),
z.object({
type: z.literal(FieldType.SIGNATURE),
value: z.string().nullable(),
}),
]);
export const ZSignEnvelopeFieldRequestSchema = z.object({
token: z.string(),
fieldId: z.number(),
fieldValue: ZSignEnvelopeFieldValue,
authOptions: ZRecipientActionAuthSchema.optional(),
});
export const ZSignEnvelopeFieldResponseSchema = z.object({
signedField: ZFieldSchema.omit({
templateId: true,
documentId: true,
}).extend({
signature: SignatureSchema.nullish(),
}),
});
export type TSignEnvelopeFieldValue = z.infer<typeof ZSignEnvelopeFieldValue>;
export type TSignEnvelopeFieldRequest = z.infer<typeof ZSignEnvelopeFieldRequestSchema>;
export type TSignEnvelopeFieldResponse = z.infer<typeof ZSignEnvelopeFieldResponseSchema>;

View File

@ -0,0 +1,98 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateEnvelopeItemsRequestSchema,
ZUpdateEnvelopeItemsResponseSchema,
} from './update-envelope-items.types';
export const updateEnvelopeItemsRoute = authenticatedProcedure
.input(ZUpdateEnvelopeItemsRequestSchema)
.output(ZUpdateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { envelopeId, data } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelopeId,
},
type: null,
userId: user.id,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: true,
envelopeItems: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
if (data.length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Envelope items are required',
});
}
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
});
}
// Check that the items belong to the envelope.
const itemsBelongToEnvelope = data.every((item) =>
envelope.envelopeItems.some(({ id }) => item.envelopeItemId === id),
);
if (!itemsBelongToEnvelope) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'One or more envelope items to update do not exist',
});
}
const updatedEnvelopeItems = await Promise.all(
data.map(async ({ envelopeItemId, order, title }) =>
prisma.envelopeItem.update({
where: {
envelopeId: envelope.id,
id: envelopeItemId,
},
data: {
order,
title,
},
select: {
id: true,
order: true,
title: true,
envelopeId: true,
},
}),
),
);
// Todo: Envelope [AUDIT_LOGS]
return {
updatedEnvelopeItems,
};
});

View File

@ -0,0 +1,29 @@
import { z } from 'zod';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { ZDocumentTitleSchema } from '../document-router/schema';
export const ZUpdateEnvelopeItemsRequestSchema = z.object({
envelopeId: z.string(),
data: z
.object({
envelopeItemId: z.string().describe('The ID of the envelope item to update.'),
order: z.number().int().min(1).optional(),
title: ZDocumentTitleSchema.optional(),
})
.array()
.min(1),
});
export const ZUpdateEnvelopeItemsResponseSchema = z.object({
updatedEnvelopeItems: EnvelopeItemSchema.pick({
id: true,
order: true,
title: true,
envelopeId: true,
}).array(),
});
export type TUpdateEnvelopeItemsRequest = z.infer<typeof ZUpdateEnvelopeItemsRequestSchema>;
export type TUpdateEnvelopeItemsResponse = z.infer<typeof ZUpdateEnvelopeItemsResponseSchema>;

View File

@ -0,0 +1,36 @@
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateEnvelopeRequestSchema,
ZUpdateEnvelopeResponseSchema,
} from './update-envelope.types';
export const updateEnvelopeRoute = authenticatedProcedure
// .meta(updateEnvelopeTrpcMeta)
.input(ZUpdateEnvelopeRequestSchema)
.output(ZUpdateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { envelopeId, data, meta = {} } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const userId = ctx.user.id;
return await updateEnvelope({
userId,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
data,
meta,
requestMetadata: ctx.metadata,
});
});

View File

@ -0,0 +1,46 @@
import { EnvelopeType } from '@prisma/client';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeLiteSchema } from '@documenso/lib/types/envelope';
import {
ZDocumentExternalIdSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from '../document-router/schema';
// export const updateEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/update',
// summary: 'Update envelope',
// tags: ['Envelope'],
// },
// };
export const ZUpdateEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
data: z
.object({
title: ZDocumentTitleSchema.optional(),
externalId: ZDocumentExternalIdSchema.nullish(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
folderId: z.string().nullish(),
})
.optional(),
meta: ZDocumentMetaUpdateSchema.optional(),
});
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
export type TUpdateEnvelopeRequest = z.infer<typeof ZUpdateEnvelopeRequestSchema>;
export type TUpdateEnvelopeResponse = z.infer<typeof ZUpdateEnvelopeResponseSchema>;

View File

@ -1,5 +1,6 @@
import { createDocumentFields } from '@documenso/lib/server-only/field/create-document-fields';
import { createTemplateFields } from '@documenso/lib/server-only/field/create-template-fields';
import { EnvelopeType } from '@prisma/client';
import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields';
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
import { deleteTemplateField } from '@documenso/lib/server-only/field/delete-template-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
@ -72,6 +73,7 @@ export const fieldRouter = router({
userId: ctx.user.id,
teamId,
fieldId,
envelopeType: EnvelopeType.DOCUMENT,
});
}),
@ -100,10 +102,13 @@ export const fieldRouter = router({
},
});
const createdFields = await createDocumentFields({
const createdFields = await createEnvelopeFields({
userId: ctx.user.id,
teamId,
documentId,
id: {
type: 'documentId',
id: documentId,
},
fields: [field],
requestMetadata: ctx.metadata,
});
@ -136,10 +141,13 @@ export const fieldRouter = router({
},
});
return await createDocumentFields({
return await createEnvelopeFields({
userId: ctx.user.id,
teamId,
documentId,
id: {
type: 'documentId',
id: documentId,
},
fields,
requestMetadata: ctx.metadata,
});
@ -251,10 +259,8 @@ export const fieldRouter = router({
/**
* @private
*
* Todo: Refactor to setFieldsForDocument function.
*/
addFields: authenticatedProcedure
setFieldsForDocument: authenticatedProcedure
.input(ZSetDocumentFieldsRequestSchema)
.output(ZSetDocumentFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
@ -268,12 +274,16 @@ export const fieldRouter = router({
});
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
teamId,
id: {
type: 'documentId',
id: documentId,
},
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
id: field.id,
recipientId: field.recipientId,
envelopeItemId: field.envelopeItemId,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
@ -311,11 +321,15 @@ export const fieldRouter = router({
},
});
const createdFields = await createTemplateFields({
const createdFields = await createEnvelopeFields({
userId: ctx.user.id,
teamId,
templateId,
id: {
type: 'templateId',
id: templateId,
},
fields: [field],
requestMetadata: ctx.metadata,
});
return createdFields.fields[0];
@ -351,6 +365,7 @@ export const fieldRouter = router({
userId: ctx.user.id,
teamId,
fieldId,
envelopeType: EnvelopeType.TEMPLATE,
});
}),
@ -379,11 +394,15 @@ export const fieldRouter = router({
},
});
return await createTemplateFields({
return await createEnvelopeFields({
userId: ctx.user.id,
teamId,
templateId,
id: {
type: 'templateId',
id: templateId,
},
fields,
requestMetadata: ctx.metadata,
});
}),
@ -490,10 +509,8 @@ export const fieldRouter = router({
/**
* @private
*
* Todo: Refactor to setFieldsForTemplate.
*/
addTemplateFields: authenticatedProcedure
setFieldsForTemplate: authenticatedProcedure
.input(ZSetFieldsForTemplateRequestSchema)
.output(ZSetFieldsForTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
@ -507,12 +524,16 @@ export const fieldRouter = router({
});
return await setFieldsForTemplate({
templateId,
id: {
type: 'templateId',
id: templateId,
},
userId: ctx.user.id,
teamId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
id: field.id,
recipientId: field.recipientId,
envelopeItemId: field.envelopeItemId,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,

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