feat: allow editing pending envelope titles (#2604)

This commit is contained in:
David Nguyen
2026-03-19 14:03:30 +11:00
committed by GitHub
parent 0b605d61c6
commit 48fb066b9a
16 changed files with 860 additions and 60 deletions
@@ -16,7 +16,7 @@ import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { nanoid } from '@documenso/lib/universal/id';
import { PRESIGNED_ENVELOPE_ITEM_ID_PREFIX } from '@documenso/lib/utils/embed-config';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { procedure } from '../trpc';
@@ -191,10 +191,36 @@ export const updateEmbeddingEnvelopeRoute = procedure
// Should be safe to use stale envelope.recipients since only signed or sent
// recipients affect the outcome.
if (willEnvelopeItemsBeModified && !canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
if (willEnvelopeItemsBeModified) {
const permissions = getEnvelopeItemPermissions(envelope, envelope.recipients);
const hasFileChange = envelopeItemIdsToDelete.length > 0 || envelopeItemsToCreate.length > 0;
const hasOrderChange = envelopeItemsToUpdate.some((item) => {
const existing = envelope.envelopeItems.find((e) => e.id === item.envelopeItemId);
return !existing || item.order !== existing.order;
});
const hasTitleChange = envelopeItemsToUpdate.some((item) => item.title !== undefined);
if (hasFileChange && !permissions.canFileBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item files are not editable',
});
}
if (hasOrderChange && !permissions.canOrderBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item order is not editable',
});
}
if (hasTitleChange && !permissions.canTitleBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item title is not editable',
});
}
}
if (envelopeItemIdsToDelete.length > 0) {
@@ -252,7 +278,14 @@ export const updateEmbeddingEnvelopeRoute = procedure
if (envelopeItemsToUpdate.length > 0) {
await UNSAFE_updateEnvelopeItems({
envelopeId: envelope.id,
envelopeType: envelope.type,
existingEnvelopeItems: envelope.envelopeItems,
data: envelopeItemsToUpdate,
user: {
name: apiToken.user.name,
email: apiToken.user.email,
},
apiRequestMetadata: ctx.metadata,
});
}
@@ -1,7 +1,7 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { UNSAFE_createEnvelopeItems } from '@documenso/lib/server-only/envelope-item/create-envelope-items';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
@@ -63,7 +63,9 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
});
}
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
const { canFileBeChanged } = getEnvelopeItemPermissions(envelope, envelope.recipients);
if (!canFileBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
});
@@ -1,7 +1,7 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { UNSAFE_deleteEnvelopeItem } from '@documenso/lib/server-only/envelope-item/delete-envelope-item';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { ZGenericSuccessResponse } from '../schema';
@@ -50,7 +50,9 @@ export const deleteEnvelopeItemRoute = authenticatedProcedure
});
}
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
const { canFileBeChanged } = getEnvelopeItemPermissions(envelope, envelope.recipients);
if (!canFileBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
});
@@ -1,7 +1,7 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { UNSAFE_replaceEnvelopeItemPdf } from '@documenso/lib/server-only/envelope-item/replace-envelope-item-pdf';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
@@ -65,7 +65,9 @@ export const replaceEnvelopeItemPdfRoute = authenticatedProcedure
});
}
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
const { canFileBeChanged } = getEnvelopeItemPermissions(envelope, envelope.recipients);
if (!canFileBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
});
@@ -1,7 +1,7 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { UNSAFE_updateEnvelopeItems } from '@documenso/lib/server-only/envelope-item/update-envelope-items';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
@@ -55,12 +55,40 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
});
}
// Note: This logic is duplicated in many places. If we plan to allow changing title/order
// even after the envelope has been sent, make sure to update it everywhere including
// embedding routes.
if (!canEnvelopeItemsBeModified(envelope, envelope.recipients)) {
const permissions = getEnvelopeItemPermissions(envelope, envelope.recipients);
const hasOrderChange = data.some((item) => {
if (item.order === undefined) {
return false;
}
const existingItem = envelope.envelopeItems.find((e) => e.id === item.envelopeItemId);
return !existingItem || existingItem.order !== item.order;
});
const hasTitleChange = data.some((item) => item.title !== undefined);
if (!hasTitleChange && !hasOrderChange) {
return {
data: envelope.envelopeItems.map((item) => ({
id: item.id,
order: item.order,
title: item.title,
envelopeId: item.envelopeId,
})),
};
}
if (hasTitleChange && !permissions.canTitleBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item is not editable',
message: 'Envelope item title is not editable',
});
}
if (hasOrderChange && !permissions.canOrderBeChanged) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item order is not editable',
});
}
@@ -77,7 +105,14 @@ export const updateEnvelopeItemsRoute = authenticatedProcedure
const updatedEnvelopeItems = await UNSAFE_updateEnvelopeItems({
envelopeId,
envelopeType: envelope.type,
existingEnvelopeItems: envelope.envelopeItems,
data,
user: {
name: user.name,
email: user.email,
},
apiRequestMetadata: ctx.metadata,
});
return {
+8 -4
View File
@@ -73,7 +73,7 @@ const t = initTRPC
/**
* Middlewares
*/
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path, meta }) => {
const infoToLog: TrpcApiLog = {
path,
auth: ctx.metadata.auth,
@@ -84,8 +84,10 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path })
const authorizationHeader = ctx.req.headers.get('authorization');
const isApiV2 = Boolean(meta?.openapi?.path);
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
if (authorizationHeader) {
if (authorizationHeader && isApiV2) {
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
@@ -164,7 +166,7 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path })
});
});
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, path, meta }) => {
// Recreate the logger with a sub request ID to differentiate between batched requests.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
@@ -180,8 +182,10 @@ export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, pat
const authorizationHeader = ctx.req.headers.get('authorization');
const isApiV2 = Boolean(meta?.openapi?.path);
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
if (authorizationHeader) {
if (authorizationHeader && isApiV2) {
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);