diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index a5dc9e23e..613146b99 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client'; +import type { DocumentData, DocumentMeta, Field, Recipient, User } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { trpc } from '@documenso/trpc/react'; @@ -29,6 +29,7 @@ export type EditDocumentFormProps = { user: User; document: DocumentWithData; recipients: Recipient[]; + documentMeta: DocumentMeta | null; fields: Field[]; documentData: DocumentData; }; @@ -41,6 +42,7 @@ export const EditDocumentForm = ({ document, recipients, fields, + documentMeta, user: _user, documentData, }: EditDocumentFormProps) => { @@ -185,7 +187,7 @@ export const EditDocumentForm = ({ gradient > - + diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index b26b6308c..b19e1cf4b 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -13,6 +13,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document'; import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { DocumentStatus } from '~/components/formatter/document-status'; +import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id'; export type DocumentPageProps = { params: { @@ -41,6 +42,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { } const { documentData } = document; + const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); const [recipients, fields] = await Promise.all([ getRecipientsForDocument({ @@ -83,6 +85,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { className="mt-8" document={document} user={user} + documentMeta={documentMeta} recipients={recipients} fields={fields} documentData={documentData} @@ -91,7 +94,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { {document.status === InternalDocumentStatus.COMPLETED && (
- +
)} diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index efd0b266c..80d88ce40 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -101,7 +101,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp gradient > - + diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts index 34c33e7cd..3c12bcb35 100644 --- a/packages/lib/server-only/document-meta/upsert-document-meta.ts +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -4,10 +4,11 @@ import { prisma } from '@documenso/prisma'; export type CreateDocumentMetaOptions = { documentId: number; - subject: string; - message: string; - timezone: string; - dateFormat: string; + subject?: string; + message?: string; + timezone?: string; + documentPassword?: string; + dateFormat?: string; userId: number; }; @@ -18,6 +19,7 @@ export const upsertDocumentMeta = async ({ dateFormat, documentId, userId, + documentPassword, }: CreateDocumentMetaOptions) => { await prisma.document.findFirstOrThrow({ where: { @@ -35,12 +37,14 @@ export const upsertDocumentMeta = async ({ message, dateFormat, timezone, + documentPassword, documentId, }, update: { subject, message, dateFormat, + documentPassword, timezone, }, }); diff --git a/packages/lib/server-only/document/duplicate-document-by-id.ts b/packages/lib/server-only/document/duplicate-document-by-id.ts index 5986b4cfe..6cdc5bc49 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -26,6 +26,7 @@ export const duplicateDocumentById = async ({ id, userId }: DuplicateDocumentByI message: true, subject: true, dateFormat: true, + documentPassword: true, timezone: true, }, }, diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index f0bfc6fda..59a92f296 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -162,6 +162,7 @@ model DocumentMeta { subject String? message String? timezone String? @db.Text @default("Etc/UTC") + documentPassword String? dateFormat String? @db.Text @default("yyyy-MM-dd hh:mm a") documentId Int @unique document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index b4a1b60e3..717f8bed2 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -24,6 +24,7 @@ import { ZSearchDocumentsMutationSchema, ZSendDocumentMutationSchema, ZSetFieldsForDocumentMutationSchema, + ZSetPasswordForDocumentMutationSchema, ZSetRecipientsForDocumentMutationSchema, ZSetTitleForDocumentMutationSchema, } from './schema'; @@ -174,6 +175,29 @@ export const documentRouter = router({ }); } }), + + setDocumentPassword: authenticatedProcedure + .input(ZSetPasswordForDocumentMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { documentId, documentPassword } = input; + await upsertDocumentMeta({ + documentId, + documentPassword, + userId: ctx.user.id, + }); + + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to send this document. Please try again later.', + }); + } + }), + + sendDocument: authenticatedProcedure .input(ZSendDocumentMutationSchema) diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 4559f65f3..baccc6b85 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -73,6 +73,11 @@ export const ZSendDocumentMutationSchema = z.object({ }), }); +export const ZSetPasswordForDocumentMutationSchema = z.object({ + documentId: z.number(), + documentPassword: z.string(), +}); + export const ZResendDocumentMutationSchema = z.object({ documentId: z.number(), recipients: z.array(z.number()).min(1), diff --git a/packages/ui/primitives/pdf-viewer.tsx b/packages/ui/primitives/pdf-viewer.tsx index be2d0cc4a..b109dca24 100644 --- a/packages/ui/primitives/pdf-viewer.tsx +++ b/packages/ui/primitives/pdf-viewer.tsx @@ -10,11 +10,13 @@ import 'react-pdf/dist/esm/Page/TextLayer.css'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import type { DocumentData } from '@documenso/prisma/client'; +import type { DocumentData, DocumentMeta } from '@documenso/prisma/client'; import { cn } from '../lib/utils'; import { PasswordDialog } from './document-password-dialog'; import { useToast } from './use-toast'; +import { trpc } from '@documenso/trpc/react'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; export type LoadedPDFDocument = PDFDocumentProxy; @@ -44,6 +46,8 @@ const PDFLoader = () => ( export type PDFViewerProps = { className?: string; documentData: DocumentData; + document?: DocumentWithData; + documentMeta?: DocumentMeta | null; onDocumentLoad?: (_doc: LoadedPDFDocument) => void; onPageClick?: OnPDFViewerPageClick; [key: string]: unknown; @@ -52,6 +56,8 @@ export type PDFViewerProps = { export const PDFViewer = ({ className, documentData, + document, + documentMeta, onDocumentLoad, onPageClick, ...props @@ -62,7 +68,7 @@ export const PDFViewer = ({ const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); - const [password, setPassword] = useState(null); + const [password, setPassword] = useState(documentMeta?.documentPassword || null); const passwordCallbackRef = useRef<((password: string | null) => void) | null>(null); const [isPasswordError, setIsPasswordError] = useState(false); const [documentBytes, setDocumentBytes] = useState(null); @@ -76,6 +82,9 @@ export const PDFViewer = ({ [documentData.data, documentData.type], ); + const {mutateAsync: addDocumentPassword } = trpc.document.setDocumentPassword.useMutation(); + + const isLoading = isDocumentBytesLoading || !documentBytes; const onDocumentLoaded = (doc: LoadedPDFDocument) => { @@ -83,12 +92,20 @@ export const PDFViewer = ({ onDocumentLoad?.(doc); }; - const onPasswordSubmit = () => { + const onPasswordSubmit = async() => { setIsPasswordModalOpen(false); - if (passwordCallbackRef.current) { - passwordCallbackRef.current(password); + try{ + await addDocumentPassword({ + documentId: document?.id ?? 0, + documentPassword: password!, + }); + passwordCallbackRef.current?.(password); + } catch (error) { + console.error('Error adding document password:', error); + } finally { passwordCallbackRef.current = null; } + }; const onDocumentPageClick = ( @@ -189,6 +206,11 @@ export const PDFViewer = ({ 'h-[80vh] max-h-[60rem]': numPages === 0, })} onPassword={(callback, reason) => { + // If the documentMeta already has a password, we don't need to ask for it again. + if(password && reason !== PasswordResponses.INCORRECT_PASSWORD){ + callback(password); + return; + } setIsPasswordModalOpen(true); passwordCallbackRef.current = callback; switch (reason) {