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 613146b99..2159b87f2 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -58,6 +58,8 @@ export const EditDocumentForm = ({ const { mutateAsync: addFields } = trpc.field.addFields.useMutation(); const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation(); const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation(); + const { mutateAsync: setPasswordForDocument } = + trpc.document.setPasswordForDocument.useMutation(); const documentFlow: Record = { title: { @@ -178,6 +180,13 @@ export const EditDocumentForm = ({ } }; + const onPasswordSubmit = async (password: string) => { + await setPasswordForDocument({ + documentId: document.id, + password, + }); + }; + const currentDocumentFlow = documentFlow[step]; return ( @@ -187,7 +196,13 @@ 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 b19e1cf4b..4df8453da 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -13,7 +13,6 @@ 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,8 +40,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) { redirect('/documents'); } - const { documentData } = document; - const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); + const { documentData, documentMeta } = document; const [recipients, fields] = await Promise.all([ getRecipientsForDocument({ @@ -94,7 +92,12 @@ 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 80d88ce40..f8b68d652 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -101,7 +101,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp gradient > - + diff --git a/package-lock.json b/package-lock.json index e3c1139f6..69825e8d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19869,7 +19869,8 @@ "react-rnd": "^10.4.1", "tailwind-merge": "^1.12.0", "tailwindcss-animate": "^1.0.5", - "ts-pattern": "^5.0.5" + "ts-pattern": "^5.0.5", + "zod": "^3.22.4" }, "devDependencies": { "@documenso/tailwind-config": "*", 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 3c12bcb35..b67c6848b 100644 --- a/packages/lib/server-only/document-meta/upsert-document-meta.ts +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -7,7 +7,7 @@ export type CreateDocumentMetaOptions = { subject?: string; message?: string; timezone?: string; - documentPassword?: string; + password?: string; dateFormat?: string; userId: number; }; @@ -19,7 +19,7 @@ export const upsertDocumentMeta = async ({ dateFormat, documentId, userId, - documentPassword, + password, }: CreateDocumentMetaOptions) => { await prisma.document.findFirstOrThrow({ where: { @@ -37,14 +37,14 @@ export const upsertDocumentMeta = async ({ message, dateFormat, timezone, - documentPassword, + password, documentId, }, update: { subject, message, dateFormat, - documentPassword, + password, 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 6cdc5bc49..ddb70b1cb 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -26,7 +26,7 @@ export const duplicateDocumentById = async ({ id, userId }: DuplicateDocumentByI message: true, subject: true, dateFormat: true, - documentPassword: true, + password: true, timezone: true, }, }, diff --git a/packages/prisma/migrations/20240115031508_add_password_to_document_meta/migration.sql b/packages/prisma/migrations/20240115031508_add_password_to_document_meta/migration.sql new file mode 100644 index 000000000..c2f5150bc --- /dev/null +++ b/packages/prisma/migrations/20240115031508_add_password_to_document_meta/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "DocumentMeta" ADD COLUMN "password" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 59a92f296..e1549e072 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -162,7 +162,7 @@ model DocumentMeta { subject String? message String? timezone String? @db.Text @default("Etc/UTC") - documentPassword String? + password 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 717f8bed2..bdc10a604 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -175,29 +175,27 @@ 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.', - }); - } - }), - + setPasswordForDocument: authenticatedProcedure + .input(ZSetPasswordForDocumentMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { documentId, password } = input; + + await upsertDocumentMeta({ + documentId, + password, + userId: ctx.user.id, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to set the password for 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 baccc6b85..c4389bdfb 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -75,9 +75,13 @@ export const ZSendDocumentMutationSchema = z.object({ export const ZSetPasswordForDocumentMutationSchema = z.object({ documentId: z.number(), - documentPassword: z.string(), + password: z.string(), }); +export type TSetPasswordForDocumentMutationSchema = z.infer< + typeof ZSetPasswordForDocumentMutationSchema +>; + export const ZResendDocumentMutationSchema = z.object({ documentId: z.number(), recipients: z.array(z.number()).min(1), diff --git a/packages/ui/package.json b/packages/ui/package.json index ce452091e..34675ba89 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -70,6 +70,7 @@ "react-rnd": "^10.4.1", "tailwind-merge": "^1.12.0", "tailwindcss-animate": "^1.0.5", - "ts-pattern": "^5.0.5" + "ts-pattern": "^5.0.5", + "zod": "^3.22.4" } } diff --git a/packages/ui/primitives/document-password-dialog.tsx b/packages/ui/primitives/document-password-dialog.tsx index 08a5de8f3..571c81716 100644 --- a/packages/ui/primitives/document-password-dialog.tsx +++ b/packages/ui/primitives/document-password-dialog.tsx @@ -1,55 +1,95 @@ -import React from 'react'; +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { Button } from './button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from './dialog'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog'; +import { Form, FormControl, FormField, FormItem, FormMessage } from './form/form'; import { Input } from './input'; +const ZPasswordDialogFormSchema = z.object({ + password: z.string(), +}); + +type TPasswordDialogFormSchema = z.infer; + type PasswordDialogProps = { open: boolean; onOpenChange: (_open: boolean) => void; - setPassword: (_password: string) => void; - onPasswordSubmit: () => void; + defaultPassword?: string; + onPasswordSubmit?: (password: string) => void; isError?: boolean; }; export const PasswordDialog = ({ open, onOpenChange, + defaultPassword, onPasswordSubmit, isError, - setPassword, }: PasswordDialogProps) => { + const form = useForm({ + defaultValues: { + password: defaultPassword ?? '', + }, + resolver: zodResolver(ZPasswordDialogFormSchema), + }); + + const onFormSubmit = ({ password }: TPasswordDialogFormSchema) => { + onPasswordSubmit?.(password); + }; + + useEffect(() => { + if (isError) { + form.setError('password', { + type: 'manual', + message: 'The password you have entered is incorrect. Please try again.', + }); + } + }, [form, isError]); + return ( - - + + Password Required + This document is password protected. Please enter the password to view the document. - - setPassword(e.target.value)} - autoComplete="off" - /> - - - {isError && ( - - The password you entered is incorrect. Please try again. - - )} + +
+ +
+ ( + + + + + + + + )} + /> + +
+ +
+
+
+
); diff --git a/packages/ui/primitives/pdf-viewer.tsx b/packages/ui/primitives/pdf-viewer.tsx index b109dca24..b4e5c10ba 100644 --- a/packages/ui/primitives/pdf-viewer.tsx +++ b/packages/ui/primitives/pdf-viewer.tsx @@ -7,16 +7,16 @@ import { type PDFDocumentProxy, PasswordResponses } from 'pdfjs-dist'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; import 'react-pdf/dist/esm/Page/TextLayer.css'; +import { match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import type { DocumentData, DocumentMeta } from '@documenso/prisma/client'; +import type { DocumentData } from '@documenso/prisma/client'; +import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; 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; @@ -47,7 +47,8 @@ export type PDFViewerProps = { className?: string; documentData: DocumentData; document?: DocumentWithData; - documentMeta?: DocumentMeta | null; + password?: string | null; + onPasswordSubmit?: (password: string) => void | Promise; onDocumentLoad?: (_doc: LoadedPDFDocument) => void; onPageClick?: OnPDFViewerPageClick; [key: string]: unknown; @@ -56,8 +57,8 @@ export type PDFViewerProps = { export const PDFViewer = ({ className, documentData, - document, - documentMeta, + password: defaultPassword, + onPasswordSubmit, onDocumentLoad, onPageClick, ...props @@ -66,10 +67,10 @@ export const PDFViewer = ({ const $el = useRef(null); + const passwordCallbackRef = useRef<((password: string | null) => void) | null>(null); + const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); - 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); @@ -82,9 +83,6 @@ export const PDFViewer = ({ [documentData.data, documentData.type], ); - const {mutateAsync: addDocumentPassword } = trpc.document.setDocumentPassword.useMutation(); - - const isLoading = isDocumentBytesLoading || !documentBytes; const onDocumentLoaded = (doc: LoadedPDFDocument) => { @@ -92,22 +90,6 @@ export const PDFViewer = ({ onDocumentLoad?.(doc); }; - const onPasswordSubmit = async() => { - setIsPasswordModalOpen(false); - 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 = ( event: React.MouseEvent, pageNumber: number, @@ -206,23 +188,19 @@ 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); + // If the document already has a password, we don't need to ask for it again. + if (defaultPassword && reason !== PasswordResponses.INCORRECT_PASSWORD) { + callback(defaultPassword); return; } + setIsPasswordModalOpen(true); + passwordCallbackRef.current = callback; - switch (reason) { - case PasswordResponses.NEED_PASSWORD: - setIsPasswordError(false); - break; - case PasswordResponses.INCORRECT_PASSWORD: - setIsPasswordError(true); - break; - default: - break; - } + + match(reason) + .with(PasswordResponses.NEED_PASSWORD, () => setIsPasswordError(false)) + .with(PasswordResponses.INCORRECT_PASSWORD, () => setIsPasswordError(true)); }} onLoadSuccess={(d) => onDocumentLoaded(d)} // Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop. @@ -270,12 +248,18 @@ export const PDFViewer = ({ ))} + { + passwordCallbackRef.current?.(password); + + setIsPasswordModalOpen(false); + + void onPasswordSubmit?.(password); + }} isError={isPasswordError} - setPassword={setPassword} /> )}