Merge branch 'main' into feat/add-pdf-image-renderer

This commit is contained in:
David Nguyen
2026-02-04 12:50:40 +11:00
committed by GitHub
125 changed files with 7880 additions and 938 deletions
@@ -0,0 +1,17 @@
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
import { adminProcedure } from '../trpc';
import { ZResyncLicenseRequestSchema, ZResyncLicenseResponseSchema } from './resync-license.types';
export const resyncLicenseRoute = adminProcedure
.input(ZResyncLicenseRequestSchema)
.output(ZResyncLicenseResponseSchema)
.mutation(async () => {
const client = LicenseClient.getInstance();
if (!client) {
return;
}
await client.resync();
});
@@ -0,0 +1,8 @@
import { z } from 'zod';
export const ZResyncLicenseRequestSchema = z.void();
export const ZResyncLicenseResponseSchema = z.void();
export type TResyncLicenseRequest = z.infer<typeof ZResyncLicenseRequestSchema>;
export type TResyncLicenseResponse = z.infer<typeof ZResyncLicenseResponseSchema>;
@@ -17,6 +17,7 @@ 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 { resyncLicenseRoute } from './resync-license';
import { updateAdminOrganisationRoute } from './update-admin-organisation';
import { updateOrganisationMemberRoleRoute } from './update-organisation-member-role';
import { updateRecipientRoute } from './update-recipient';
@@ -44,6 +45,9 @@ export const adminRouter = router({
stripe: {
createCustomer: createStripeCustomerRoute,
},
license: {
resync: resyncLicenseRoute,
},
user: {
get: getUserRoute,
update: updateUserRoute,
@@ -38,6 +38,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
meta,
folderId,
attachments,
formValues,
} = input;
const { remaining } = await getServerLimits({ userId: user.id, teamId });
@@ -68,6 +69,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure
title,
externalId,
visibility,
formValues,
globalAccessAuth,
globalActionAuth,
recipients: (recipients || []).map((recipient) => ({
@@ -27,6 +27,7 @@ import {
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
* @deprecated
*/
export const createDocumentTemporaryMeta: TrpcRouteMeta = {
openapi: {
@@ -36,6 +37,7 @@ export const createDocumentTemporaryMeta: TrpcRouteMeta = {
description:
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
tags: ['Document'],
deprecated: true,
},
};
@@ -78,6 +78,7 @@ export const createDocumentRoute = authenticatedProcedure
visibility,
globalAccessAuth,
globalActionAuth,
formValues,
recipients: (recipients || []).map((recipient) => ({
...recipient,
fields: (recipient.fields || []).map((field) => ({
@@ -1,11 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
import { authenticatedProcedure } from '../trpc';
import {
@@ -42,12 +40,26 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure
});
}
const encrypted = encryptSecondaryData({
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
const certificatePdf = await generateAuditLogPdf({
envelope,
recipients: envelope.recipients,
fields: envelope.fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
envelopeItems: envelope.envelopeItems.map((item) => item.title),
pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height,
});
const result = await certificatePdf.save();
const base64 = Buffer.from(result).toString('base64');
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
data: base64,
envelopeTitle: envelope.title,
};
});
@@ -5,7 +5,8 @@ export const ZDownloadDocumentAuditLogsRequestSchema = z.object({
});
export const ZDownloadDocumentAuditLogsResponseSchema = z.object({
url: z.string(),
data: z.string(),
envelopeTitle: z.string(),
});
export type TDownloadDocumentAuditLogsRequest = z.infer<
@@ -1,12 +1,11 @@
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 { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
@@ -27,7 +26,7 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
},
});
const envelope = await getEnvelopeById({
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'documentId',
id: documentId,
@@ -37,16 +36,54 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: {
include: {
signature: true,
},
},
documentMeta: true,
user: {
select: {
email: true,
name: true,
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
if (!isDocumentCompleted(envelope.status)) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const encrypted = encryptSecondaryData({
data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
const certificatePdf = await generateCertificatePdf({
envelope,
recipients: envelope.recipients,
fields: envelope.fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height,
});
const result = await certificatePdf.save();
const base64 = Buffer.from(result).toString('base64');
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
data: base64,
envelopeTitle: envelope.title,
};
});
@@ -5,7 +5,8 @@ export const ZDownloadDocumentCertificateRequestSchema = z.object({
});
export const ZDownloadDocumentCertificateResponseSchema = z.object({
url: z.string(),
data: z.string(),
envelopeTitle: z.string(),
});
export type TDownloadDocumentCertificateRequest = z.infer<
@@ -0,0 +1,110 @@
import { EnvelopeType } from '@prisma/client';
import pMap from 'p-map';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZBulkDeleteEnvelopesRequestSchema,
ZBulkDeleteEnvelopesResponseSchema,
} from './bulk-delete-envelopes.types';
export const bulkDeleteEnvelopesRoute = authenticatedProcedure
// .meta(bulkDeleteEnvelopesMeta) // Keeping this as a private API for a little while until we're sure it's stable and the request/response schemas are finalized.
.input(ZBulkDeleteEnvelopesRequestSchema)
.output(ZBulkDeleteEnvelopesResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeIds } = input;
ctx.logger.info({
input: {
envelopeIds,
},
});
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
ids: {
type: 'envelopeId',
ids: envelopeIds,
},
userId: user.id,
teamId,
type: null,
});
const envelopes = await prisma.envelope.findMany({
where: envelopeWhereInput,
select: {
id: true,
type: true,
},
});
const results = await pMap(
envelopes,
async (envelope) => {
const { id: envelopeId, type: envelopeType } = envelope;
try {
if (envelopeType === EnvelopeType.DOCUMENT) {
await deleteDocument({
id: {
type: 'envelopeId',
id: envelopeId,
},
userId: user.id,
teamId,
requestMetadata: ctx.metadata,
});
} else if (envelopeType === EnvelopeType.TEMPLATE) {
await deleteTemplate({
id: {
type: 'envelopeId',
id: envelopeId,
},
userId: user.id,
teamId,
});
}
return {
success: true,
envelopeId,
};
} catch (err) {
ctx.logger.warn(
{
envelopeId,
error: err,
},
'Failed to delete envelope during bulk delete',
);
return {
success: false,
envelopeId,
};
}
},
{
concurrency: 10,
stopOnError: false,
},
);
const deletedCount = results.filter((r) => r.success).length;
const failedIds = results.filter((r) => !r.success).map((r) => r.envelopeId);
// Include envelope IDs that were not attempted (unauthorized/not found)
const attemptedIds = new Set(envelopes.map((e) => e.id));
const unattemptedIds = envelopeIds.filter((id) => !attemptedIds.has(id));
return {
deletedCount,
failedIds: [...failedIds, ...unattemptedIds],
};
});
@@ -0,0 +1,31 @@
import { z } from 'zod';
// READ ME: IF YOU UNCOMMENT THIS THEN UNSKIP THE TEST IN api-access-envelope-bulk.spec.ts
// Keeping this as a private API for a little while until we're sure it's stable and the request/response schemas are finalized.
// export const bulkDeleteEnvelopesMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/bulk/delete',
// summary: 'Bulk delete envelopes',
// description: 'Delete multiple envelopes.',
// tags: ['Envelopes'],
// },
// };
export const ZBulkDeleteEnvelopesRequestSchema = z.object({
envelopeIds: z
.array(z.string())
.min(1)
.max(100)
.describe(
'The IDs of the envelopes to delete. The maximum number of envelopes you can delete at once is 100.',
),
});
export const ZBulkDeleteEnvelopesResponseSchema = z.object({
deletedCount: z.number(),
failedIds: z.array(z.string()),
});
export type TBulkDeleteEnvelopesRequest = z.infer<typeof ZBulkDeleteEnvelopesRequestSchema>;
export type TBulkDeleteEnvelopesResponse = z.infer<typeof ZBulkDeleteEnvelopesResponseSchema>;
@@ -0,0 +1,73 @@
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZBulkMoveEnvelopesRequestSchema,
ZBulkMoveEnvelopesResponseSchema,
} from './bulk-move-envelopes.types';
export const bulkMoveEnvelopesRoute = authenticatedProcedure
// .meta(bulkMoveEnvelopesMeta)
.input(ZBulkMoveEnvelopesRequestSchema)
.output(ZBulkMoveEnvelopesResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeIds, envelopeType, folderId } = input;
ctx.logger.info({
input: {
envelopeIds,
envelopeType,
folderId,
},
});
// Build the where input for the update query.
const { envelopeWhereInput, team } = await getMultipleEnvelopeWhereInput({
ids: {
type: 'envelopeId',
ids: envelopeIds,
},
userId: user.id,
teamId,
type: envelopeType,
});
// Validate folder access if moving to a folder (not root).
if (folderId) {
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
team: buildTeamWhereQuery({
teamId,
userId: user.id,
}),
type: envelopeType,
visibility: {
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
},
},
});
if (!folder) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found or access denied',
});
}
}
const result = await prisma.envelope.updateMany({
where: envelopeWhereInput,
data: {
folderId: folderId,
},
});
return {
movedCount: result.count,
};
});
@@ -0,0 +1,38 @@
import { EnvelopeType } from '@prisma/client';
import { z } from 'zod';
// READ ME: IF YOU UNCOMMENT THIS THEN UNSKIP THE TEST IN api-access-envelope-bulk.spec.ts
// Keeping this as a private API for a little while until we're sure it's stable and the request/response schemas are finalized.
// export const bulkMoveEnvelopesMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/bulk/move',
// summary: 'Bulk move envelopes to folder',
// description: 'Move multiple envelopes to a specified folder.',
// tags: ['Envelopes'],
// },
// };
export const ZBulkMoveEnvelopesRequestSchema = z.object({
envelopeIds: z
.array(z.string())
.min(1)
.max(100)
.describe(
'The IDs of the envelopes to move. The maximum number of envelopes you can move at once is 100.',
),
envelopeType: z.nativeEnum(EnvelopeType).describe('The type of the envelopes being moved.'),
folderId: z
.string()
.nullable()
.describe(
'The ID of the folder to move the envelopes to. If null envelopes will be moved to the root folder.',
),
});
export const ZBulkMoveEnvelopesResponseSchema = z.object({
movedCount: z.number().describe('The number of envelopes that were moved.'),
});
export type TBulkMoveEnvelopesRequest = z.infer<typeof ZBulkMoveEnvelopesRequestSchema>;
export type TBulkMoveEnvelopesResponse = z.infer<typeof ZBulkMoveEnvelopesResponseSchema>;
@@ -1,8 +1,15 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import {
convertPlaceholdersToFieldInputs,
extractPdfPlaceholders,
} from '@documenso/lib/server-only/pdf/auto-place-fields';
import { findRecipientByPlaceholder } from '@documenso/lib/server-only/pdf/helpers';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { prefixedId } from '@documenso/lib/universal/id';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
@@ -84,14 +91,31 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
});
}
// For each file, stream to s3 and create the document data.
// For each file: normalize, extract & clean placeholders, then upload.
const envelopeItems = await Promise.all(
files.map(async (file) => {
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
let buffer = Buffer.from(await file.arrayBuffer());
if (envelope.formValues) {
buffer = await insertFormValuesInPdf({ pdf: buffer, formValues: envelope.formValues });
}
const normalized = await normalizePdf(buffer, {
flattenForm: envelope.type !== 'TEMPLATE',
});
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
const { id: documentDataId } = await putPdfFileServerSide({
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(cleanedPdf),
});
return {
title: file.name,
documentDataId,
placeholders,
};
}),
);
@@ -131,6 +155,65 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
),
});
// Create fields from placeholders if the envelope already has recipients.
if (envelope.recipients.length > 0) {
const orderedRecipients = [...envelope.recipients].sort((a, b) => {
const aOrder = a.signingOrder ?? Number.MAX_SAFE_INTEGER;
const bOrder = b.signingOrder ?? Number.MAX_SAFE_INTEGER;
if (aOrder !== bOrder) {
return aOrder - bOrder;
}
return a.id - b.id;
});
for (const uploadedItem of envelopeItems) {
if (!uploadedItem.placeholders || uploadedItem.placeholders.length === 0) {
continue;
}
const createdItem = createdItems.find(
(ci) => ci.documentDataId === uploadedItem.documentDataId,
);
if (!createdItem) {
continue;
}
const fieldsToCreate = convertPlaceholdersToFieldInputs(
uploadedItem.placeholders,
(recipientPlaceholder, placeholder) =>
findRecipientByPlaceholder(
recipientPlaceholder,
placeholder,
orderedRecipients,
orderedRecipients,
),
createdItem.id,
);
if (fieldsToCreate.length > 0) {
await tx.field.createMany({
data: fieldsToCreate.map((field) => ({
envelopeId: envelope.id,
envelopeItemId: createdItem.id,
recipientId: field.recipientId,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta || undefined,
})),
});
}
}
}
return createdItems;
});
@@ -3,7 +3,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 { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
import { authenticatedProcedure } from '../trpc';
@@ -69,7 +71,7 @@ export const createEnvelopeRoute = authenticatedProcedure
});
}
// For each file, stream to s3 and create the document data.
// For each file: normalize, extract & clean placeholders, then upload.
const envelopeItems = await Promise.all(
files.map(async (file) => {
let pdf = Buffer.from(await file.arrayBuffer());
@@ -82,20 +84,22 @@ export const createEnvelopeRoute = authenticatedProcedure
});
}
const { id: documentDataId } = await putNormalizedPdfFileServerSide(
{
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdf),
},
{
flattenForm: type !== EnvelopeType.TEMPLATE,
},
);
const normalized = await normalizePdf(pdf, {
flattenForm: type !== EnvelopeType.TEMPLATE,
});
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
const { id: documentDataId } = await putPdfFileServerSide({
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(cleanedPdf),
});
return {
title: file.name,
documentDataId,
placeholders,
};
}),
);
@@ -22,7 +22,7 @@ export const createEnvelopeFieldsMeta: TrpcRouteMeta = {
},
};
const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
const ZCreateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for'),
envelopeItemId: z
@@ -31,14 +31,51 @@ const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
.describe(
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
),
page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
}),
);
/**
* Position a field using explicit percentage-based coordinates.
*/
const ZCoordinatePositionSchema = z.object({
page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
});
/**
* Position a field using a PDF text placeholder (e.g. "{{name}}").
*
* The placeholder text is matched in the envelope item's PDF and the field is
* placed at the bounding box of that match. Width and height can optionally be
* overridden; when omitted the dimensions of the placeholder text are used.
*/
const ZPlaceholderPositionSchema = z.object({
placeholder: z
.string()
.describe(
'Text to search for in the PDF (e.g. "{{name}}"). The field will be placed at the location of this text.',
),
width: ZClampedFieldWidthSchema.optional().describe(
'Override the width of the field. When omitted, the width of the placeholder text is used.',
),
height: ZClampedFieldHeightSchema.optional().describe(
'Override the height of the field. When omitted, the height of the placeholder text is used.',
),
matchAll: z
.boolean()
.optional()
.describe(
'When true, creates a field at every occurrence of the placeholder in the PDF. When false or omitted, only the first occurrence is used.',
),
});
const ZCreateFieldSchema = ZCreateFieldBaseSchema.and(
z.union([ZCoordinatePositionSchema, ZPlaceholderPositionSchema]),
);
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
data: ZCreateFieldSchema.array(),
@@ -3,6 +3,8 @@ 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 { bulkDeleteEnvelopesRoute } from './bulk-delete-envelopes';
import { bulkMoveEnvelopesRoute } from './bulk-move-envelopes';
import { createEnvelopeRoute } from './create-envelope';
import { createEnvelopeItemsRoute } from './create-envelope-items';
import { deleteEnvelopeRoute } from './delete-envelope';
@@ -72,6 +74,10 @@ export const envelopeRouter = router({
auditLog: {
find: findEnvelopeAuditLogsRoute,
},
bulk: {
move: bulkMoveEnvelopesRoute,
delete: bulkDeleteEnvelopesRoute,
},
get: getEnvelopeRoute,
getMany: getEnvelopesByIdsRoute,
create: createEnvelopeRoute,
@@ -100,13 +100,15 @@ export const deleteOrganisationMembers = async ({
organisationId,
},
});
});
for (const member of membersToDelete) {
await jobs.triggerJob({
name: 'send.organisation-member-left.email',
payload: {
organisationId,
memberUserId: userId,
memberUserId: member.userId,
},
});
});
}
};
@@ -90,10 +90,12 @@ export const profileRouter = router({
const parsedTeamId = teamId ? Number(teamId) : null;
if (Number.isNaN(parsedTeamId)) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid team ID provided',
});
if (typeof parsedTeamId === 'number') {
if (Number.isNaN(parsedTeamId) || parsedTeamId <= 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid team ID provided',
});
}
}
return await submitSupportTicket({