mirror of
https://github.com/documenso/documenso.git
synced 2026-08-23 23:02:22 +10:00
feat: add pending signed pdf downloads for recipients
Recipients can download a PDF that contains the inserted fields while the envelope is in the PENDING state. The token file route accepts the `pending` version. The PDF contains only the fields from recipients who signed and the fields of the recipient who makes the request. This is the same set of fields that the signing page shows to the recipient. The signing page and the completion page show the download dialog for pending envelopes. The dialog does not show the "Partial" button for legacy envelopes.
This commit is contained in:
@@ -83,16 +83,7 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
},
|
||||
},
|
||||
include: {
|
||||
envelope: {
|
||||
include: {
|
||||
recipients: {
|
||||
select: {
|
||||
role: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
envelope: true,
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
DocumentStatus,
|
||||
type EnvelopeType,
|
||||
EnvelopeType as EnvelopeTypeEnum,
|
||||
type RecipientRole,
|
||||
type SigningStatus,
|
||||
SigningStatus,
|
||||
type TemplateType,
|
||||
TemplateType as TemplateTypeEnum,
|
||||
} from '@prisma/client';
|
||||
@@ -55,17 +54,13 @@ type EnvelopeForPendingDownload = {
|
||||
id: string;
|
||||
status: DocumentStatus;
|
||||
internalVersion: number;
|
||||
recipients: Array<{
|
||||
role: RecipientRole;
|
||||
signingStatus: SigningStatus;
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options shape varies by `version`:
|
||||
* - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers.
|
||||
* - `pending`: generates a fresh PDF with currently-inserted fields burned in; needs the
|
||||
* full envelope (id, status, internalVersion, recipients) plus envelopeItemId to query fields.
|
||||
* envelope (id, status, internalVersion) plus envelopeItemId to query fields.
|
||||
*/
|
||||
type HandleEnvelopeItemFileRequestOptions = {
|
||||
title: string;
|
||||
@@ -81,6 +76,13 @@ type HandleEnvelopeItemFileRequestOptions = {
|
||||
version: 'pending';
|
||||
envelopeItemId: string;
|
||||
envelope: EnvelopeForPendingDownload;
|
||||
|
||||
/**
|
||||
* When set, only fields from recipients who have signed, plus the fields of
|
||||
* the recipient owning this token, are burned in. Keeps recipient downloads
|
||||
* in parity with what the signing page shows them.
|
||||
*/
|
||||
recipientToken?: string;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -165,6 +167,7 @@ const handlePendingFileRequest = async ({
|
||||
envelopeItemId,
|
||||
envelope,
|
||||
documentData,
|
||||
recipientToken,
|
||||
context: c,
|
||||
}: PendingFileRequestOptions) => {
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
@@ -191,6 +194,13 @@ const handlePendingFileRequest = async ({
|
||||
where: {
|
||||
envelopeItemId,
|
||||
inserted: true,
|
||||
...(recipientToken
|
||||
? {
|
||||
recipient: {
|
||||
OR: [{ signingStatus: SigningStatus.SIGNED }, { token: recipientToken }],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
|
||||
@@ -160,12 +160,6 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
select: {
|
||||
role: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -289,52 +283,80 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
'/token/:token/envelopeItem/:envelopeItemId/download/:version?',
|
||||
sValidator('param', ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema),
|
||||
async (c) => {
|
||||
const { token, envelopeItemId, version } = c.req.valid('param');
|
||||
const logger = c.get('logger');
|
||||
|
||||
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const { token, envelopeItemId, version } = c.req.valid('param');
|
||||
|
||||
if (token.startsWith('qr_')) {
|
||||
envelopeWhereQuery = {
|
||||
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
qrToken: token,
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (token.startsWith('qr_')) {
|
||||
envelopeWhereQuery = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
qrToken: token,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||
where: envelopeWhereQuery,
|
||||
include: {
|
||||
envelope: true,
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelopeItem) {
|
||||
return c.json({ error: 'Envelope item not found' }, 404);
|
||||
}
|
||||
|
||||
if (!envelopeItem.documentData) {
|
||||
return c.json({ error: 'Document data not found' }, 404);
|
||||
}
|
||||
|
||||
const baseOptions = {
|
||||
title: envelopeItem.title,
|
||||
documentData: envelopeItem.documentData,
|
||||
isDownload: true,
|
||||
context: c,
|
||||
} as const;
|
||||
|
||||
if (version === 'pending') {
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
...baseOptions,
|
||||
version,
|
||||
envelopeItemId: envelopeItem.id,
|
||||
envelope: envelopeItem.envelope,
|
||||
recipientToken: token,
|
||||
});
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
...baseOptions,
|
||||
version,
|
||||
status: envelopeItem.envelope.status,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
const { status, body } = AppError.toRestAPIError(error);
|
||||
|
||||
return c.json({ error: body.message, code: error.code }, status);
|
||||
}
|
||||
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
|
||||
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||
where: envelopeWhereQuery,
|
||||
include: {
|
||||
envelope: true,
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelopeItem) {
|
||||
return c.json({ error: 'Envelope item not found' }, 404);
|
||||
}
|
||||
|
||||
if (!envelopeItem.documentData) {
|
||||
return c.json({ error: 'Document data not found' }, 404);
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
title: envelopeItem.title,
|
||||
status: envelopeItem.envelope.status,
|
||||
documentData: envelopeItem.documentData,
|
||||
version,
|
||||
isDownload: true,
|
||||
context: c,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export type TGetEnvelopeItemFileDownloadRequestParams = z.infer<typeof ZGetEnvel
|
||||
export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
envelopeItemId: z.string().min(1),
|
||||
version: z.enum(['signed', 'original']).default('signed'),
|
||||
version: z.enum(['signed', 'original', 'pending']).default('signed'),
|
||||
});
|
||||
|
||||
export type TGetEnvelopeItemFileTokenDownloadRequestParams = z.infer<
|
||||
|
||||
Reference in New Issue
Block a user