diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index f7b96be71..7e6cf26b8 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -8,7 +8,6 @@ import { useSession } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; -import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { type Document, type Field, type Recipient, RecipientRole } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; @@ -27,9 +26,10 @@ export type SigningFormProps = { document: Document; recipient: Recipient; fields: Field[]; + redirectUrl?: string | null; }; -export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => { +export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => { const router = useRouter(); const analytics = useAnalytics(); const { data: session } = useSession(); @@ -56,7 +56,6 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = }; const onFormSubmit = async () => { - const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); setValidateUninsertedFields(true); const isFieldsValid = validateFieldsInserted(fields); @@ -75,9 +74,8 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = documentId: document.id, timestamp: new Date().toISOString(), }); - documentMeta?.redirectUrl - ? router.push(documentMeta.redirectUrl) - : router.push(`/sign/${recipient.token}/complete`); + + redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`); }; return ( diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index a1e1388cd..9a7e8acbe 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -8,7 +8,6 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; -import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; @@ -49,15 +48,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp viewedDocument({ token }).catch(() => null), ]); - const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); - if (!document || !document.documentData || !recipient) { return notFound(); } const truncatedTitle = truncateTitle(document.title); - const { documentData } = document; + const { documentData, documentMeta } = document; const { user } = await getServerComponentSession(); @@ -65,8 +62,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp document.status === DocumentStatus.COMPLETED || recipient.signingStatus === SigningStatus.SIGNED ) { - // - redirect(`/sign/${token}/complete`); + documentMeta?.redirectUrl + ? redirect(documentMeta.redirectUrl) + : redirect(`/sign/${token}/complete`); } if (documentMeta?.password) { @@ -134,7 +132,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
- +
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 25bfbbb40..23c0a38c0 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,4 +1,5 @@ -import { NextRequest, NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; import { getToken } from 'next-auth/jwt'; diff --git a/package-lock.json b/package-lock.json index 9012d3f29..618dc4ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14610,6 +14610,7 @@ "version": "6.9.7", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -19602,14 +19603,14 @@ "@react-email/section": "0.0.10", "@react-email/tailwind": "0.0.9", "@react-email/text": "0.0.6", - "nodemailer": "^6.9.3", + "nodemailer": "^6.9.9", "react-email": "^1.9.5", "resend": "^2.0.0" }, "devDependencies": { "@documenso/tailwind-config": "*", "@documenso/tsconfig": "*", - "@types/nodemailer": "^6.4.8", + "@types/nodemailer": "^6.4.14", "tsup": "^7.1.0" } }, @@ -19627,6 +19628,14 @@ "node": ">=16.0.0" } }, + "packages/email/node_modules/nodemailer": { + "version": "6.9.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz", + "integrity": "sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==", + "engines": { + "node": ">=6.0.0" + } + }, "packages/eslint-config": { "name": "@documenso/eslint-config", "version": "0.0.0", diff --git a/packages/email/package.json b/packages/email/package.json index d41a4c24c..984ea3d4c 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -35,14 +35,14 @@ "@react-email/section": "0.0.10", "@react-email/tailwind": "0.0.9", "@react-email/text": "0.0.6", - "nodemailer": "^6.9.3", + "nodemailer": "^6.9.9", "react-email": "^1.9.5", "resend": "^2.0.0" }, "devDependencies": { "@documenso/tailwind-config": "*", "@documenso/tsconfig": "*", - "@types/nodemailer": "^6.4.8", + "@types/nodemailer": "^6.4.14", "tsup": "^7.1.0" } } diff --git a/packages/lib/constants/url-regex.ts b/packages/lib/constants/url-regex.ts new file mode 100644 index 000000000..259ce070d --- /dev/null +++ b/packages/lib/constants/url-regex.ts @@ -0,0 +1,2 @@ +export const URL_REGEX = + /^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i; 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 ddb70b1cb..146d9d8fa 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -28,6 +28,7 @@ export const duplicateDocumentById = async ({ id, userId }: DuplicateDocumentByI dateFormat: true, password: true, timezone: true, + redirectUrl: true, }, }, }, diff --git a/packages/lib/server-only/document/get-document-by-token.ts b/packages/lib/server-only/document/get-document-by-token.ts index 62c8a5ca1..18f9a5161 100644 --- a/packages/lib/server-only/document/get-document-by-token.ts +++ b/packages/lib/server-only/document/get-document-by-token.ts @@ -27,6 +27,7 @@ export const getDocumentAndSenderByToken = async ({ include: { User: true, documentData: true, + documentMeta: true, }, }); diff --git a/packages/prisma/migrations/20240131120410_add_document_meta_redirect_url/migration.sql b/packages/prisma/migrations/20240206111230_add_document_meta_redirect_url/migration.sql similarity index 100% rename from packages/prisma/migrations/20240131120410_add_document_meta_redirect_url/migration.sql rename to packages/prisma/migrations/20240206111230_add_document_meta_redirect_url/migration.sql diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 5e9706b74..7096769b8 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -192,7 +192,7 @@ model DocumentMeta { dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text documentId Int @unique document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) - redirectUrl String? @db.Text + redirectUrl String? } enum ReadStatus { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 8f63ebb9d..ff2c83a48 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; +import { URL_REGEX } from '@documenso/lib/constants/url-regex'; import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client'; export const ZGetDocumentByIdQuerySchema = z.object({ @@ -71,7 +72,12 @@ export const ZSendDocumentMutationSchema = z.object({ message: z.string(), timezone: z.string(), dateFormat: z.string(), - redirectUrl: z.string().optional(), + redirectUrl: z + .string() + .optional() + .refine((value) => value === undefined || URL_REGEX.test(value), { + message: 'Please enter a valid URL', + }), }), }); diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 740dad6c4..7ce77710c 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; +import { Info } from 'lucide-react'; import { Controller, useForm } from 'react-hook-form'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; @@ -23,6 +24,7 @@ import { SelectTrigger, SelectValue, } from '@documenso/ui/primitives/select'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Combobox } from '../combobox'; import { FormErrorMessage } from '../form/form-error-message'; @@ -69,7 +71,6 @@ export const AddSubjectFormPartial = ({ message: document.documentMeta?.message ?? '', timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, - redirectUrl: document.documentMeta?.redirectUrl ?? '', }, }, }); @@ -164,86 +165,94 @@ export const AddSubjectFormPartial = ({ - {hasDateField && ( - - - - Advanced Options - + + + + Advanced Options + - -
- + + {hasDateField && ( + <> +
+ - ( - + + + - - {DATE_FORMATS.map((format) => ( - - {format.label} - - ))} - - - )} - /> -
+ + {DATE_FORMATS.map((format) => ( + + {format.label} + + ))} + + + )} + /> +
-
- +
+ - ( - value && onChange(value)} - disabled={documentHasBeenSent} - /> - )} - /> -
+ ( + value && onChange(value)} + disabled={documentHasBeenSent} + /> + )} + /> +
+ + )} -
-
-
- +
+
+
+ - -
+ + +
- - - - )} +
+ + +
diff --git a/packages/ui/primitives/document-flow/add-subject.types.ts b/packages/ui/primitives/document-flow/add-subject.types.ts index 285b8f813..fd4175368 100644 --- a/packages/ui/primitives/document-flow/add-subject.types.ts +++ b/packages/ui/primitives/document-flow/add-subject.types.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import { URL_REGEX } from '@documenso/lib/constants/url-regex'; export const ZAddSubjectFormSchema = z.object({ meta: z.object({ @@ -9,7 +10,12 @@ export const ZAddSubjectFormSchema = z.object({ message: z.string(), timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE), dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT), - redirectUrl: z.string().optional(), + redirectUrl: z + .string() + .optional() + .refine((value) => value === undefined || URL_REGEX.test(value), { + message: 'Please enter a valid URL', + }), }), });