From 2ae94b1e55a8f1fc697134ec6f23eecfa205305f Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Wed, 10 Sep 2025 13:13:33 +0300 Subject: [PATCH] feat: add api routes and restore previous work --- ...teamUrl.download.audit-logs.$documentId.ts | 62 ++++++++++++++++ ...eamUrl.download.certificate.$documentId.ts | 70 +++++++++++++++++++ .../download-document-certificate.ts | 48 ++++--------- .../download-document-certificate.types.ts | 4 +- 4 files changed, 147 insertions(+), 37 deletions(-) create mode 100644 apps/remix/app/routes/api+/t.$teamUrl.download.audit-logs.$documentId.ts create mode 100644 apps/remix/app/routes/api+/t.$teamUrl.download.certificate.$documentId.ts diff --git a/apps/remix/app/routes/api+/t.$teamUrl.download.audit-logs.$documentId.ts b/apps/remix/app/routes/api+/t.$teamUrl.download.audit-logs.$documentId.ts new file mode 100644 index 000000000..a5831ff41 --- /dev/null +++ b/apps/remix/app/routes/api+/t.$teamUrl.download.audit-logs.$documentId.ts @@ -0,0 +1,62 @@ +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getAuditLogsPdf } from '@documenso/lib/server-only/htmltopdf/get-audit-logs-pdf'; +import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; + +import type { Route } from './+types/t.$teamUrl.download.audit-logs.$documentId'; + +export async function loader({ request, params }: Route.LoaderArgs) { + const documentId = Number(params.documentId); + const teamUrl = params.teamUrl; + + if (!documentId || !teamUrl) { + return Response.json({ error: 'Invalid document ID or team URL' }, { status: 400 }); + } + + try { + const { user } = await getSession(request); + + const team = await getTeamByUrl({ userId: user.id, teamUrl }); + + if (!team) { + return Response.json({ error: 'Team not found or access denied' }, { status: 404 }); + } + + const document = await getDocumentById({ + documentId, + userId: user.id, + teamId: team.id, + }).catch(() => null); + + if (!document || (team.id && document.teamId !== team.id)) { + return Response.json({ error: 'Document not found or access denied' }, { status: 404 }); + } + + const pdfBuffer = await getAuditLogsPdf({ + documentId: document.id, + language: document.documentMeta?.language, + }); + + const filename = `${document.title.replace(/\.pdf$/, '')}_audit_logs.pdf`; + + return new Response(pdfBuffer, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Length': pdfBuffer.length.toString(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache', + Expires: '0', + }, + }); + } catch (error) { + if (error instanceof AppError) { + const statusCode = error.code === AppErrorCode.UNAUTHORIZED ? 401 : 400; + + return Response.json({ error: error.message }, { status: statusCode }); + } + + return Response.json({ error: 'Failed to generate audit logs PDF' }, { status: 500 }); + } +} diff --git a/apps/remix/app/routes/api+/t.$teamUrl.download.certificate.$documentId.ts b/apps/remix/app/routes/api+/t.$teamUrl.download.certificate.$documentId.ts new file mode 100644 index 000000000..df947c32b --- /dev/null +++ b/apps/remix/app/routes/api+/t.$teamUrl.download.certificate.$documentId.ts @@ -0,0 +1,70 @@ +import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getCertificatePdf } from '@documenso/lib/server-only/htmltopdf/get-certificate-pdf'; +import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { isDocumentCompleted } from '@documenso/lib/utils/document'; + +import type { Route } from './+types/t.$teamUrl.download.certificate.$documentId'; + +export async function loader({ request, params }: Route.LoaderArgs) { + const documentId = Number(params.documentId); + const teamUrl = params.teamUrl; + + if (!documentId || !teamUrl) { + return Response.json({ error: 'Invalid document ID or team URL' }, { status: 400 }); + } + + try { + const { user } = await getSession(request); + + const team = await getTeamByUrl({ userId: user.id, teamUrl }).catch(() => null); + + if (!team) { + return Response.json({ error: 'Team not found or access denied' }, { status: 404 }); + } + + const document = await getDocumentById({ + documentId, + userId: user.id, + teamId: team.id, + }).catch(() => null); + + if (!document || document.teamId !== team.id) { + return Response.json({ error: 'Document not found or access denied' }, { status: 404 }); + } + + if (!isDocumentCompleted(document.status)) { + return Response.json( + { error: 'Document must be completed to download the certificate' }, + { status: 400 }, + ); + } + + const pdfBuffer = await getCertificatePdf({ + documentId: document.id, + language: document.documentMeta?.language, + }); + + const filename = `${document.title.replace(/\.pdf$/, '')}_certificate.pdf`; + + return new Response(pdfBuffer, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Length': pdfBuffer.length.toString(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache', + Expires: '0', + }, + }); + } catch (error) { + if (error instanceof AppError) { + const statusCode = error.code === AppErrorCode.UNAUTHORIZED ? 401 : 400; + + return Response.json({ error: error.message }, { status: statusCode }); + } + + return Response.json({ error: 'Failed to generate certificate PDF' }, { status: 500 }); + } +} diff --git a/packages/trpc/server/document-router/download-document-certificate.ts b/packages/trpc/server/document-router/download-document-certificate.ts index 7b22e080c..b59eafbf0 100644 --- a/packages/trpc/server/document-router/download-document-certificate.ts +++ b/packages/trpc/server/document-router/download-document-certificate.ts @@ -1,6 +1,9 @@ -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +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 { getCertificatePdf } from '@documenso/lib/server-only/htmltopdf/get-certificate-pdf'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { authenticatedProcedure } from '../trpc'; @@ -13,7 +16,7 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure .input(ZDownloadDocumentCertificateRequestSchema) .output(ZDownloadDocumentCertificateResponseSchema) .mutation(async ({ input, ctx }) => { - const { teamId, user } = ctx; + const { teamId } = ctx; const { documentId } = input; ctx.logger.info({ @@ -24,43 +27,20 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure const document = await getDocumentById({ documentId, - userId: user.id, + userId: ctx.user.id, teamId, }); - if (!document || (teamId && document.teamId !== teamId)) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'You do not have access to this document.', - }); - } - if (!isDocumentCompleted(document.status)) { throw new AppError('DOCUMENT_NOT_COMPLETE'); } - try { - const pdfBuffer = await getCertificatePdf({ - documentId: document.id, - language: document.documentMeta?.language, - }); + const encrypted = encryptSecondaryData({ + data: document.id.toString(), + expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(), + }); - const base64Pdf = pdfBuffer.toString('base64'); - const filename = `${document.title.replace(/\.pdf$/, '')}_certificate.pdf`; - - return { - pdfData: base64Pdf, - filename, - contentType: 'application/pdf', - }; - } catch (error) { - ctx.logger.error({ - error, - message: 'Failed to generate certificate PDF', - documentId, - }); - - throw new AppError(AppErrorCode.UNKNOWN_ERROR, { - message: 'Failed to generate certificate PDF', - }); - } + return { + url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`, + }; }); diff --git a/packages/trpc/server/document-router/download-document-certificate.types.ts b/packages/trpc/server/document-router/download-document-certificate.types.ts index 74e57e95c..df81f1cad 100644 --- a/packages/trpc/server/document-router/download-document-certificate.types.ts +++ b/packages/trpc/server/document-router/download-document-certificate.types.ts @@ -5,9 +5,7 @@ export const ZDownloadDocumentCertificateRequestSchema = z.object({ }); export const ZDownloadDocumentCertificateResponseSchema = z.object({ - pdfData: z.string(), - filename: z.string(), - contentType: z.string(), + url: z.string(), }); export type TDownloadDocumentCertificateRequest = z.infer<