mirror of
https://github.com/documenso/documenso.git
synced 2026-07-23 00:13:46 +10:00
chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/document-file-conversion. Conflicts were format-only (Tailwind class ordering, single-line vs multi-line) plus two semantic merges: - files.helpers.ts: combine main's pending-PDF download path with the branch's original-source-file (DOCX/PNG/JPEG) download path - download-pdf.ts: combine main's versionToFilenameSuffix helper with the branch's server-provided Content-Disposition filename support
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type TDetectFieldsRequest,
|
||||
ZNormalizedFieldWithContextSchema,
|
||||
} from './detect-fields.types';
|
||||
import { type TDetectFieldsRequest, ZNormalizedFieldWithContextSchema } from './detect-fields.types';
|
||||
|
||||
export type { TDetectFieldsRequest };
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { streamText } from 'hono/streaming';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { IS_AI_FEATURES_CONFIGURED } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { detectFieldsFromEnvelope } from '@documenso/lib/server-only/ai/envelope/detect-fields';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { streamText } from 'hono/streaming';
|
||||
|
||||
import type { HonoEnv } from '../../router';
|
||||
import { ZDetectFieldsRequestSchema } from './detect-fields.types';
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { ZConfidenceLevel, ZDetectableFieldType } from '@documenso/lib/server-only/ai/envelope/detect-fields/schema';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
ZConfidenceLevel,
|
||||
ZDetectableFieldType,
|
||||
} from '@documenso/lib/server-only/ai/envelope/detect-fields/schema';
|
||||
|
||||
export const ZDetectFieldsRequestSchema = z.object({
|
||||
envelopeId: z.string().min(1).describe('The ID of the envelope to detect fields from.'),
|
||||
teamId: z.number().describe('The ID of the team the envelope belongs to.'),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ZDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema';
|
||||
|
||||
import { type TDetectRecipientsRequest } from './detect-recipients.types';
|
||||
import type { TDetectRecipientsRequest } from './detect-recipients.types';
|
||||
|
||||
export type { TDetectRecipientsRequest };
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { streamText } from 'hono/streaming';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { IS_AI_FEATURES_CONFIGURED } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { detectRecipientsFromEnvelope } from '@documenso/lib/server-only/ai/envelope/detect-recipients';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { streamText } from 'hono/streaming';
|
||||
|
||||
import type { HonoEnv } from '../../router';
|
||||
import { ZDetectRecipientsRequestSchema } from './detect-recipients.types';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDetectRecipientsRequestSchema = z.object({
|
||||
envelopeId: z.string().min(1).describe('The ID of the envelope to detect recipients from.'),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import type { HonoEnv } from '../../router';
|
||||
import { handleEnvelopeItemFileRequest } from '../files/files.helpers';
|
||||
import {
|
||||
ZDownloadDocumentRequestParamsSchema,
|
||||
ZDownloadEnvelopeItemRequestParamsSchema,
|
||||
ZDownloadEnvelopeItemRequestQuerySchema,
|
||||
} from './download.types';
|
||||
|
||||
export const downloadRoute = new Hono<HonoEnv>()
|
||||
@@ -23,11 +23,13 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
.get(
|
||||
'/envelope/item/:envelopeItemId/download',
|
||||
sValidator('param', ZDownloadEnvelopeItemRequestParamsSchema),
|
||||
sValidator('query', ZDownloadEnvelopeItemRequestQuerySchema),
|
||||
async (c) => {
|
||||
const logger = c.get('logger');
|
||||
|
||||
try {
|
||||
const { envelopeItemId, version } = c.req.valid('param');
|
||||
const { envelopeItemId } = c.req.valid('param');
|
||||
const { version } = c.req.valid('query');
|
||||
const authorizationHeader = c.req.header('authorization');
|
||||
|
||||
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||
@@ -65,7 +67,16 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
},
|
||||
},
|
||||
include: {
|
||||
envelope: true,
|
||||
envelope: {
|
||||
include: {
|
||||
recipients: {
|
||||
select: {
|
||||
role: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
@@ -78,23 +89,36 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
return c.json({ error: 'Document data not found' }, 404);
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
const baseOptions = {
|
||||
title: envelopeItem.title,
|
||||
status: envelopeItem.envelope.status,
|
||||
documentData: envelopeItem.documentData,
|
||||
version: version || 'signed',
|
||||
isDownload: true,
|
||||
context: c,
|
||||
} as const;
|
||||
|
||||
if (version === 'pending') {
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
...baseOptions,
|
||||
version,
|
||||
envelopeItemId: envelopeItem.id,
|
||||
envelope: envelopeItem.envelope,
|
||||
});
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
...baseOptions,
|
||||
version,
|
||||
status: envelopeItem.envelope.status,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
return c.json({ error: error.message }, 401);
|
||||
}
|
||||
const { status, body } = AppError.toRestAPIError(error);
|
||||
|
||||
return c.json({ error: error.message }, 400);
|
||||
// Preserve the existing `{ error }` shape for backwards compatibility;
|
||||
// `code` is added as a new field for callers that want to branch on it.
|
||||
return c.json({ error: body.message, code: error.code }, status);
|
||||
}
|
||||
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
@@ -105,88 +129,84 @@ export const downloadRoute = new Hono<HonoEnv>()
|
||||
* Download a document by its ID.
|
||||
* Requires API key authentication via Authorization header.
|
||||
*/
|
||||
.get(
|
||||
'/document/:documentId/download',
|
||||
sValidator('param', ZDownloadDocumentRequestParamsSchema),
|
||||
async (c) => {
|
||||
const logger = c.get('logger');
|
||||
.get('/document/:documentId/download', sValidator('param', ZDownloadDocumentRequestParamsSchema), async (c) => {
|
||||
const logger = c.get('logger');
|
||||
|
||||
try {
|
||||
const { documentId, version } = c.req.valid('param');
|
||||
const authorizationHeader = c.req.header('authorization');
|
||||
try {
|
||||
const { documentId, version } = c.req.valid('param');
|
||||
const authorizationHeader = c.req.header('authorization');
|
||||
|
||||
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||
|
||||
if (!token) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'API token was not provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await getApiTokenByToken({ token });
|
||||
|
||||
if (apiToken.user.disabled) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User is disabled',
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({
|
||||
auth: 'api',
|
||||
source: 'apiV2',
|
||||
path: c.req.path,
|
||||
userId: apiToken.user.id,
|
||||
apiTokenId: apiToken.id,
|
||||
documentId,
|
||||
version,
|
||||
if (!token) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'API token was not provided',
|
||||
});
|
||||
|
||||
const envelope = await getEnvelopeById({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: apiToken.user.id,
|
||||
teamId: apiToken.teamId,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!envelope) {
|
||||
return c.json({ error: 'Document not found' }, 404);
|
||||
}
|
||||
|
||||
// Get the first envelope item (documents have exactly one)
|
||||
const [envelopeItem] = envelope.envelopeItems;
|
||||
|
||||
if (!envelopeItem) {
|
||||
return c.json({ error: 'Document item not found' }, 404);
|
||||
}
|
||||
|
||||
if (!envelopeItem.documentData) {
|
||||
return c.json({ error: 'Document data not found' }, 404);
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
title: envelopeItem.title,
|
||||
status: envelope.status,
|
||||
documentData: envelopeItem.documentData,
|
||||
version: version || 'signed',
|
||||
isDownload: true,
|
||||
context: c,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
return c.json({ error: error.message }, 401);
|
||||
}
|
||||
|
||||
return c.json({ error: error.message }, 400);
|
||||
}
|
||||
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const apiToken = await getApiTokenByToken({ token });
|
||||
|
||||
if (apiToken.user.disabled) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User is disabled',
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({
|
||||
auth: 'api',
|
||||
source: 'apiV2',
|
||||
path: c.req.path,
|
||||
userId: apiToken.user.id,
|
||||
apiTokenId: apiToken.id,
|
||||
documentId,
|
||||
version,
|
||||
});
|
||||
|
||||
const envelope = await getEnvelopeById({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
userId: apiToken.user.id,
|
||||
teamId: apiToken.teamId,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!envelope) {
|
||||
return c.json({ error: 'Document not found' }, 404);
|
||||
}
|
||||
|
||||
// Get the first envelope item (documents have exactly one)
|
||||
const [envelopeItem] = envelope.envelopeItems;
|
||||
|
||||
if (!envelopeItem) {
|
||||
return c.json({ error: 'Document item not found' }, 404);
|
||||
}
|
||||
|
||||
if (!envelopeItem.documentData) {
|
||||
return c.json({ error: 'Document data not found' }, 404);
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
title: envelopeItem.title,
|
||||
status: envelope.status,
|
||||
documentData: envelopeItem.documentData,
|
||||
version: version || 'signed',
|
||||
isDownload: true,
|
||||
context: c,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
return c.json({ error: error.message }, 401);
|
||||
}
|
||||
|
||||
return c.json({ error: error.message }, 400);
|
||||
}
|
||||
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,18 +2,21 @@ import { z } from 'zod';
|
||||
|
||||
export const ZDownloadEnvelopeItemRequestParamsSchema = z.object({
|
||||
envelopeItemId: z.string().describe('The ID of the envelope item to download.'),
|
||||
});
|
||||
|
||||
export const ZDownloadEnvelopeItemRequestQuerySchema = z.object({
|
||||
version: z
|
||||
.enum(['original', 'signed'])
|
||||
.enum(['original', 'signed', 'pending'])
|
||||
.optional()
|
||||
.default('signed')
|
||||
.describe(
|
||||
'The version of the envelope item to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
|
||||
'The version of the envelope item to download. "signed" returns the completed document with all signatures and the audit trail, "original" returns the original uploaded document, "pending" returns the original document with currently-inserted fields burned in (only valid while the envelope is in PENDING status; not a final executed document).',
|
||||
),
|
||||
});
|
||||
|
||||
export type TDownloadEnvelopeItemRequestParams = z.infer<
|
||||
typeof ZDownloadEnvelopeItemRequestParamsSchema
|
||||
>;
|
||||
export type TDownloadEnvelopeItemRequestParams = z.infer<typeof ZDownloadEnvelopeItemRequestParamsSchema>;
|
||||
|
||||
export type TDownloadEnvelopeItemRequestQuery = z.infer<typeof ZDownloadEnvelopeItemRequestQuerySchema>;
|
||||
|
||||
export const ZDownloadDocumentRequestParamsSchema = z.object({
|
||||
documentId: z.coerce.number().describe('The ID of the document to download.'),
|
||||
|
||||
@@ -1,47 +1,92 @@
|
||||
import {
|
||||
type DocumentDataType,
|
||||
DocumentStatus,
|
||||
type EnvelopeType,
|
||||
type TemplateType,
|
||||
} from '@prisma/client';
|
||||
import { EnvelopeType as EnvelopeTypeEnum, TemplateType as TemplateTypeEnum } from '@prisma/client';
|
||||
import contentDisposition from 'content-disposition';
|
||||
import { type Context } from 'hono';
|
||||
|
||||
import { getFileExtensionForMimeType } from '@documenso/lib/constants/upload';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { sha256 } from '@documenso/lib/universal/crypto';
|
||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
type DocumentDataType,
|
||||
DocumentStatus,
|
||||
type EnvelopeType,
|
||||
EnvelopeType as EnvelopeTypeEnum,
|
||||
type RecipientRole,
|
||||
type SigningStatus,
|
||||
type TemplateType,
|
||||
TemplateType as TemplateTypeEnum,
|
||||
} from '@prisma/client';
|
||||
import contentDisposition from 'content-disposition';
|
||||
import type { Context } from 'hono';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { HonoEnv } from '../../router';
|
||||
|
||||
type HandleEnvelopeItemFileRequestOptions = {
|
||||
title: string;
|
||||
type DocumentDataInput = {
|
||||
type: DocumentDataType;
|
||||
data: string;
|
||||
initialData: string;
|
||||
originalData?: string | null;
|
||||
originalMimeType?: string | null;
|
||||
};
|
||||
|
||||
type EnvelopeForPendingDownload = {
|
||||
id: string;
|
||||
status: DocumentStatus;
|
||||
documentData: {
|
||||
type: DocumentDataType;
|
||||
data: string;
|
||||
initialData: string;
|
||||
originalData?: string | null;
|
||||
originalMimeType?: string | null;
|
||||
};
|
||||
version: 'signed' | 'original';
|
||||
isDownload: boolean;
|
||||
context: Context<HonoEnv>;
|
||||
internalVersion: number;
|
||||
recipients: Array<{
|
||||
role: RecipientRole;
|
||||
signingStatus: SigningStatus;
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to handle envelope item file requests (both view and download)
|
||||
* 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.
|
||||
*/
|
||||
export const handleEnvelopeItemFileRequest = async ({
|
||||
type HandleEnvelopeItemFileRequestOptions = {
|
||||
title: string;
|
||||
documentData: DocumentDataInput;
|
||||
isDownload: boolean;
|
||||
context: Context<HonoEnv>;
|
||||
} & (
|
||||
| {
|
||||
version: 'signed' | 'original';
|
||||
status: DocumentStatus;
|
||||
}
|
||||
| {
|
||||
version: 'pending';
|
||||
envelopeItemId: string;
|
||||
envelope: EnvelopeForPendingDownload;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Single entry point for envelope item file requests (view and download).
|
||||
*
|
||||
* Dispatches on `version`:
|
||||
* - `signed` / `original`: returns the stored PDF bytes as-is.
|
||||
* - `pending`: generates an on-demand PDF with all currently-inserted fields burned in.
|
||||
*/
|
||||
export const handleEnvelopeItemFileRequest = async (options: HandleEnvelopeItemFileRequestOptions) => {
|
||||
if (options.version === 'pending') {
|
||||
return handlePendingFileRequest(options);
|
||||
}
|
||||
|
||||
return handleStaticFileRequest(options);
|
||||
};
|
||||
|
||||
type StaticFileRequestOptions = Extract<HandleEnvelopeItemFileRequestOptions, { version: 'signed' | 'original' }>;
|
||||
|
||||
const handleStaticFileRequest = async ({
|
||||
title,
|
||||
status,
|
||||
documentData,
|
||||
version,
|
||||
isDownload,
|
||||
context: c,
|
||||
}: HandleEnvelopeItemFileRequestOptions) => {
|
||||
}: StaticFileRequestOptions) => {
|
||||
const shouldServeOriginalSourceFile =
|
||||
version === 'original' &&
|
||||
documentData.originalData &&
|
||||
@@ -54,9 +99,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
||||
? documentData.data
|
||||
: documentData.initialData;
|
||||
|
||||
const contentType = shouldServeOriginalSourceFile
|
||||
? documentData.originalMimeType!
|
||||
: 'application/pdf';
|
||||
const contentType = shouldServeOriginalSourceFile ? documentData.originalMimeType! : 'application/pdf';
|
||||
|
||||
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
||||
|
||||
@@ -112,6 +155,107 @@ export const handleEnvelopeItemFileRequest = async ({
|
||||
return c.body(file);
|
||||
};
|
||||
|
||||
type PendingFileRequestOptions = Extract<HandleEnvelopeItemFileRequestOptions, { version: 'pending' }>;
|
||||
|
||||
const handlePendingFileRequest = async ({
|
||||
title,
|
||||
envelopeItemId,
|
||||
envelope,
|
||||
documentData,
|
||||
context: c,
|
||||
}: PendingFileRequestOptions) => {
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
const errorCode = match(envelope.status)
|
||||
.with(DocumentStatus.DRAFT, () => AppErrorCode.ENVELOPE_DRAFT)
|
||||
.with(DocumentStatus.COMPLETED, () => AppErrorCode.ENVELOPE_COMPLETED)
|
||||
.with(DocumentStatus.REJECTED, () => AppErrorCode.ENVELOPE_REJECTED)
|
||||
.otherwise(() => AppErrorCode.INVALID_REQUEST);
|
||||
|
||||
throw new AppError(errorCode, {
|
||||
message: `Envelope ${envelope.id} must be pending to download a partially signed PDF`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.internalVersion !== 2) {
|
||||
throw new AppError(AppErrorCode.ENVELOPE_LEGACY, {
|
||||
message: `Envelope ${envelope.id} is a legacy envelope and does not support partially signed PDF downloads`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const fields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeItemId,
|
||||
inserted: true,
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
const etag = Buffer.from(
|
||||
sha256(
|
||||
JSON.stringify({
|
||||
envelopeStatus: envelope.status,
|
||||
fields: fields.map((field) => ({
|
||||
id: field.id,
|
||||
customText: field.customText,
|
||||
signatureId: field.signature?.id ?? null,
|
||||
signatureCreated: field.signature?.created ?? null,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
).toString('hex');
|
||||
|
||||
if (c.req.header('If-None-Match') === etag) {
|
||||
c.header('ETag', etag);
|
||||
c.header('Cache-Control', 'no-store, private');
|
||||
|
||||
return c.body(null, 304);
|
||||
}
|
||||
|
||||
const file = await getFileServerSide({
|
||||
type: documentData.type,
|
||||
data: documentData.initialData,
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
return c.json({ error: 'File not found' }, 404);
|
||||
}
|
||||
|
||||
const pdf = await generatePartialSignedPdf({
|
||||
pdfData: file,
|
||||
fields,
|
||||
});
|
||||
|
||||
c.get('logger').info({
|
||||
source: 'pendingPdfDownload',
|
||||
envelopeId: envelope.id,
|
||||
envelopeItemId,
|
||||
insertedFieldCount: fields.length,
|
||||
etag,
|
||||
});
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header('Cache-Control', 'no-store, private');
|
||||
c.header('ETag', etag);
|
||||
|
||||
const baseTitle = title.replace(/\.pdf$/i, '');
|
||||
const filename = `${baseTitle}_pending.pdf`;
|
||||
|
||||
c.header('Content-Disposition', contentDisposition(filename));
|
||||
|
||||
return c.body(pdf);
|
||||
};
|
||||
|
||||
type CheckEnvelopeFileAccessOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@@ -138,10 +282,7 @@ export const checkEnvelopeFileAccess = async ({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
envelopeType === EnvelopeTypeEnum.TEMPLATE &&
|
||||
templateType === TemplateTypeEnum.ORGANISATION
|
||||
) {
|
||||
if (envelopeType === EnvelopeTypeEnum.TEMPLATE && templateType === TemplateTypeEnum.ORGANISATION) {
|
||||
const orgAccess = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
@@ -9,6 +5,9 @@ import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embeddin
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import type { HonoEnv } from '../../router';
|
||||
import { checkEnvelopeFileAccess, handleEnvelopeItemFileRequest } from './files.helpers';
|
||||
@@ -126,10 +125,7 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
return c.json(
|
||||
{ error: 'User does not have access to the team that this envelope is associated with' },
|
||||
403,
|
||||
);
|
||||
return c.json({ error: 'User does not have access to the team that this envelope is associated with' }, 403);
|
||||
}
|
||||
|
||||
if (!envelopeItem.documentData) {
|
||||
@@ -150,66 +146,101 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
'/envelope/:envelopeId/envelopeItem/:envelopeItemId/download/:version?',
|
||||
sValidator('param', ZGetEnvelopeItemFileDownloadRequestParamsSchema),
|
||||
async (c) => {
|
||||
const { envelopeId, envelopeItemId, version } = c.req.valid('param');
|
||||
const logger = c.get('logger');
|
||||
|
||||
const session = await getOptionalSession(c);
|
||||
try {
|
||||
const { envelopeId, envelopeItemId, version } = c.req.valid('param');
|
||||
|
||||
if (!session.user) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
const session = await getOptionalSession(c);
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
where: {
|
||||
id: envelopeItemId,
|
||||
if (!session.user) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
where: {
|
||||
id: envelopeItemId,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
recipients: {
|
||||
select: {
|
||||
role: true,
|
||||
signingStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
return c.json({ error: 'Envelope not found' }, 404);
|
||||
if (!envelope) {
|
||||
return c.json({ error: 'Envelope not found' }, 404);
|
||||
}
|
||||
|
||||
const [envelopeItem] = envelope.envelopeItems;
|
||||
|
||||
if (!envelopeItem) {
|
||||
return c.json({ error: 'Envelope item not found' }, 404);
|
||||
}
|
||||
|
||||
const hasDownloadAccess = await checkEnvelopeFileAccess({
|
||||
userId: session.user.id,
|
||||
teamId: envelope.teamId,
|
||||
envelopeType: envelope.type,
|
||||
templateType: envelope.templateType,
|
||||
});
|
||||
|
||||
if (!hasDownloadAccess) {
|
||||
return c.json(
|
||||
{
|
||||
error: 'User does not have access to the team that this envelope is associated with',
|
||||
},
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
...baseOptions,
|
||||
version,
|
||||
status: 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] = envelope.envelopeItems;
|
||||
|
||||
if (!envelopeItem) {
|
||||
return c.json({ error: 'Envelope item not found' }, 404);
|
||||
}
|
||||
|
||||
const hasDownloadAccess = await checkEnvelopeFileAccess({
|
||||
userId: session.user.id,
|
||||
teamId: envelope.teamId,
|
||||
envelopeType: envelope.type,
|
||||
templateType: envelope.templateType,
|
||||
});
|
||||
|
||||
if (!hasDownloadAccess) {
|
||||
return c.json(
|
||||
{ error: 'User does not have access to the team that this envelope is associated with' },
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
if (!envelopeItem.documentData) {
|
||||
return c.json({ error: 'Document data not found' }, 404);
|
||||
}
|
||||
|
||||
return await handleEnvelopeItemFileRequest({
|
||||
title: envelopeItem.title,
|
||||
status: envelope.status,
|
||||
documentData: envelopeItem.documentData,
|
||||
version,
|
||||
isDownload: true,
|
||||
context: c,
|
||||
});
|
||||
},
|
||||
)
|
||||
.get(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZUploadPdfRequestSchema = z.object({
|
||||
file: z.instanceof(File),
|
||||
@@ -32,36 +31,28 @@ export const ZGetEnvelopeItemFileRequestParamsSchema = z.object({
|
||||
envelopeItemId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TGetEnvelopeItemFileRequestParams = z.infer<
|
||||
typeof ZGetEnvelopeItemFileRequestParamsSchema
|
||||
>;
|
||||
export type TGetEnvelopeItemFileRequestParams = z.infer<typeof ZGetEnvelopeItemFileRequestParamsSchema>;
|
||||
|
||||
export const ZGetEnvelopeItemFileRequestQuerySchema = z.object({
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TGetEnvelopeItemFileRequestQuery = z.infer<
|
||||
typeof ZGetEnvelopeItemFileRequestQuerySchema
|
||||
>;
|
||||
export type TGetEnvelopeItemFileRequestQuery = z.infer<typeof ZGetEnvelopeItemFileRequestQuerySchema>;
|
||||
|
||||
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
envelopeItemId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TGetEnvelopeItemFileTokenRequestParams = z.infer<
|
||||
typeof ZGetEnvelopeItemFileTokenRequestParamsSchema
|
||||
>;
|
||||
export type TGetEnvelopeItemFileTokenRequestParams = z.infer<typeof ZGetEnvelopeItemFileTokenRequestParamsSchema>;
|
||||
|
||||
export const ZGetEnvelopeItemFileDownloadRequestParamsSchema = z.object({
|
||||
envelopeId: 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 TGetEnvelopeItemFileDownloadRequestParams = z.infer<
|
||||
typeof ZGetEnvelopeItemFileDownloadRequestParamsSchema
|
||||
>;
|
||||
export type TGetEnvelopeItemFileDownloadRequestParams = z.infer<typeof ZGetEnvelopeItemFileDownloadRequestParamsSchema>;
|
||||
|
||||
export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { HonoEnv } from '../../../router';
|
||||
import { handleEnvelopeItemPdfRequest } from './get-envelope-item-pdf';
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { DocumentData, EnvelopeItem } from '@prisma/client';
|
||||
import { type Context, Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import type { DocumentDataVersion } from '@documenso/lib/types/document';
|
||||
import { sha256 } from '@documenso/lib/universal/crypto';
|
||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { DocumentData, EnvelopeItem } from '@prisma/client';
|
||||
import { type Context, Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { HonoEnv } from '../../../router';
|
||||
import { checkEnvelopeFileAccess } from '../files.helpers';
|
||||
|
||||
Reference in New Issue
Block a user