diff --git a/apps/web/src/app/(share)/q/[token]/document-download-button.tsx b/apps/web/src/app/(share)/q/[token]/document-download-button.tsx new file mode 100644 index 000000000..d45c21f02 --- /dev/null +++ b/apps/web/src/app/(share)/q/[token]/document-download-button.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Trans, msg } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { Download } from 'lucide-react'; + +import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; +import type { Document, DocumentData } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type DocumentDownloadButtonProps = { + document: Document & { + documentData: DocumentData; + }; +}; + +export const DocumentDownloadButton = ({ document }: DocumentDownloadButtonProps) => { + const { toast } = useToast(); + const { _ } = useLingui(); + + const onDownloadClick = async () => { + try { + if (!document) { + throw new Error('No document available'); + } + + await downloadPDF({ documentData: document.documentData, fileName: document.title }); + } catch (err) { + toast({ + title: _(msg`Something went wrong`), + description: _(msg`An error occurred while downloading your document.`), + variant: 'destructive', + }); + } + }; + + return ( + + ); +}; diff --git a/apps/web/src/app/(share)/q/[token]/layout.tsx b/apps/web/src/app/(share)/q/[token]/layout.tsx new file mode 100644 index 000000000..33b6d2bf0 --- /dev/null +++ b/apps/web/src/app/(share)/q/[token]/layout.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams'; +import { getTeams } from '@documenso/lib/server-only/team/get-teams'; + +import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header'; +import { NextAuthProvider } from '~/providers/next-auth'; + +export type SigningLayoutProps = { + children: React.ReactNode; +}; + +export default async function SigningLayout({ children }: SigningLayoutProps) { + await setupI18nSSR(); + + const { user, session } = await getServerComponentSession(); + + let teams: TGetTeamsResponse = []; + + if (user && session) { + teams = await getTeams({ userId: user.id }); + } + + return ( + +
+ {user && } + +
{children}
+
+
+ ); +} diff --git a/apps/web/src/app/(share)/q/[token]/page.tsx b/apps/web/src/app/(share)/q/[token]/page.tsx new file mode 100644 index 000000000..9cb8b2b9f --- /dev/null +++ b/apps/web/src/app/(share)/q/[token]/page.tsx @@ -0,0 +1,68 @@ +import { notFound } from 'next/navigation'; + +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; +import { getDocumentByAccessToken } from '@documenso/lib/server-only/document/get-document-by-access-token'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; + +import { DocumentDownloadButton } from './document-download-button'; + +export type DocumentAccessPageProps = { + params: { + token?: string; + }; +}; + +export default async function DocumentAccessPage({ params: { token } }: DocumentAccessPageProps) { + await setupI18nSSR(); + + if (!token) { + return notFound(); + } + + const { document } = await getDocumentByAccessToken({ token }); + const { documentData, documentMeta } = document; + + return ( +
+

+ {document.title} +

+ +
+ + + + + + +
+
+
+

Download document

+
+ +

+ Download the document as a PDF file. +

+ +
+ +
+
+
+
+
+ ); +} diff --git a/packages/lib/jobs/definitions/internal/seal-document.handler.ts b/packages/lib/jobs/definitions/internal/seal-document.handler.ts index da0c6c5be..c04836fc1 100644 --- a/packages/lib/jobs/definitions/internal/seal-document.handler.ts +++ b/packages/lib/jobs/definitions/internal/seal-document.handler.ts @@ -210,6 +210,13 @@ export const run = async ({ }, }); + await tx.documentAccessToken.create({ + data: { + token: nanoid(32), + documentId: document.id, + }, + }); + await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, diff --git a/packages/lib/server-only/document/get-document-by-access-token.ts b/packages/lib/server-only/document/get-document-by-access-token.ts new file mode 100644 index 000000000..078fd3012 --- /dev/null +++ b/packages/lib/server-only/document/get-document-by-access-token.ts @@ -0,0 +1,27 @@ +import { prisma } from '@documenso/prisma'; + +export type GetDocumentByAccessTokenOptions = { + token: string; +}; + +export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTokenOptions) => { + if (!token) { + throw new Error('Missing token'); + } + + const result = await prisma.documentAccessToken.findFirstOrThrow({ + where: { + token, + }, + include: { + document: { + include: { + documentData: true, + documentMeta: true, + }, + }, + }, + }); + + return result; +};