From 65cc26fd028d9e80d19d6b15f250bde87f11c9a7 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Tue, 21 Nov 2023 06:42:29 +0200 Subject: [PATCH] feat: email verification for registration (#599) --- .../(dashboard)/documents/upload-document.tsx | 4 +- apps/web/src/app/(dashboard)/layout.tsx | 2 + .../verify-email/[token]/page.tsx | 97 ++ .../(unauthenticated)/verify-email/page.tsx | 28 + .../layout/verify-email-banner.tsx | 123 +++ package-lock.json | 935 +++++++----------- package.json | 4 +- packages/email/package.json | 8 +- .../template-confirmation-email.tsx | 52 + packages/email/templates/confirm-email.tsx | 69 ++ packages/lib/next-auth/auth-options.ts | 4 + .../auth/send-confirmation-email.ts | 56 ++ .../user/generate-confirmation-token.ts | 41 + .../user/send-confirmation-token.ts | 41 + packages/lib/server-only/user/verify-email.ts | 70 ++ .../migration.sql | 17 + .../migration.sql | 3 + packages/prisma/schema.prisma | 13 +- packages/trpc/server/auth-router/router.ts | 7 +- packages/trpc/server/profile-router/router.ts | 23 + packages/trpc/server/profile-router/schema.ts | 5 + 21 files changed, 1004 insertions(+), 598 deletions(-) create mode 100644 apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx create mode 100644 apps/web/src/app/(unauthenticated)/verify-email/page.tsx create mode 100644 apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx create mode 100644 packages/email/template-components/template-confirmation-email.tsx create mode 100644 packages/email/templates/confirm-email.tsx create mode 100644 packages/lib/server-only/auth/send-confirmation-email.ts create mode 100644 packages/lib/server-only/user/generate-confirmation-token.ts create mode 100644 packages/lib/server-only/user/send-confirmation-token.ts create mode 100644 packages/lib/server-only/user/verify-email.ts create mode 100644 packages/prisma/migrations/20231025074705_add_email_confirmation_registration/migration.sql create mode 100644 packages/prisma/migrations/20231031072857_verify_existing_users/migration.sql diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index 644c9017a..9963d072a 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -6,6 +6,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; @@ -22,6 +23,7 @@ export type UploadDocumentProps = { export const UploadDocument = ({ className }: UploadDocumentProps) => { const router = useRouter(); + const { data: session } = useSession(); const { toast } = useToast(); @@ -79,7 +81,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx index db1bf8159..3bd79c8a7 100644 --- a/apps/web/src/app/(dashboard)/layout.tsx +++ b/apps/web/src/app/(dashboard)/layout.tsx @@ -10,6 +10,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get- import { CommandMenu } from '~/components/(dashboard)/common/command-menu'; import { Header } from '~/components/(dashboard)/layout/header'; +import { VerifyEmailBanner } from '~/components/(dashboard)/layout/verify-email-banner'; import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus'; import { NextAuthProvider } from '~/providers/next-auth'; @@ -31,6 +32,7 @@ export default async function AuthenticatedDashboardLayout({ return ( + {!user.emailVerified && }
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx new file mode 100644 index 000000000..f671fb101 --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx @@ -0,0 +1,97 @@ +import Link from 'next/link'; + +import { AlertTriangle, CheckCircle2, XCircle, XOctagon } from 'lucide-react'; + +import { verifyEmail } from '@documenso/lib/server-only/user/verify-email'; +import { Button } from '@documenso/ui/primitives/button'; + +export type PageProps = { + params: { + token: string; + }; +}; + +export default async function VerifyEmailPage({ params: { token } }: PageProps) { + if (!token) { + return ( +
+
+ +
+ +

No token provided

+

+ It seems that there is no token provided. Please check your email and try again. +

+
+ ); + } + + const verified = await verifyEmail({ token }); + + if (verified === null) { + return ( +
+
+ +
+ +
+

Something went wrong

+ +

+ We were unable to verify your email. If your email is not verified already, please try + again. +

+ + +
+
+ ); + } + + if (!verified) { + return ( +
+
+ +
+ +
+

Your token has expired!

+ +

+ It seems that the provided token has expired. We've just sent you another token, please + check your email and try again. +

+ + +
+
+ ); + } + + return ( +
+
+ +
+ +
+

Email Confirmed!

+ +

+ Your email has been successfully confirmed! You can now use all features of Documenso. +

+ + +
+
+ ); +} diff --git a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx new file mode 100644 index 000000000..04202d19b --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx @@ -0,0 +1,28 @@ +import Link from 'next/link'; + +import { XCircle } from 'lucide-react'; + +import { Button } from '@documenso/ui/primitives/button'; + +export default function EmailVerificationWithoutTokenPage() { + return ( +
+
+ +
+ +
+

Uh oh! Looks like you're missing a token

+ +

+ It seems that there is no token provided, if you are trying to verify your email please + follow the link in your email. +

+ + +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx b/apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx new file mode 100644 index 000000000..24e47c186 --- /dev/null +++ b/apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { AlertTriangle } from 'lucide-react'; + +import { ONE_SECOND } from '@documenso/lib/constants/time'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type VerifyEmailBannerProps = { + email: string; +}; + +const RESEND_CONFIRMATION_EMAIL_TIMEOUT = 20 * ONE_SECOND; + +export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => { + const { toast } = useToast(); + const [isOpen, setIsOpen] = useState(false); + + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + + const { mutateAsync: sendConfirmationEmail, isLoading } = + trpc.profile.sendConfirmationEmail.useMutation(); + + const onResendConfirmationEmail = async () => { + try { + setIsButtonDisabled(true); + + await sendConfirmationEmail({ email: email }); + + toast({ + title: 'Success', + description: 'Verification email sent successfully.', + }); + + setIsOpen(false); + setTimeout(() => setIsButtonDisabled(false), RESEND_CONFIRMATION_EMAIL_TIMEOUT); + } catch (err) { + setIsButtonDisabled(false); + + toast({ + title: 'Error', + description: 'Something went wrong while sending the confirmation email.', + variant: 'destructive', + }); + } + }; + + useEffect(() => { + // Check localStorage to see if we've recently automatically displayed the dialog + // if it was within the past 24 hours, don't show it again + // otherwise, show it again and update the localStorage timestamp + const emailVerificationDialogLastShown = localStorage.getItem( + 'emailVerificationDialogLastShown', + ); + + if (emailVerificationDialogLastShown) { + const lastShownTimestamp = parseInt(emailVerificationDialogLastShown); + + if (Date.now() - lastShownTimestamp < 24 * 60 * 60 * 1000) { + return; + } + } + + setIsOpen(true); + + localStorage.setItem('emailVerificationDialogLastShown', Date.now().toString()); + }, []); + + return ( + <> +
+
+
+ + Verify your email address to unlock all features. +
+ +
+ +
+
+
+ + + + Verify your email address + + + We've sent a confirmation email to {email}. Please check your inbox and + click the link in the email to verify your account. + + +
+ +
+
+
+ + ); +}; diff --git a/package-lock.json b/package-lock.json index 6516ade62..7b53af8c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1706,11 +1706,11 @@ "link": true }, "node_modules/@documenso/nodemailer-resend": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-1.0.0.tgz", - "integrity": "sha512-rG+jBbBEsVJUBU6v/2hb+OQD1m3Lhn49TOzQjln73zzL1B/sZsHhYOKpNPlTX0/FafCP7P9fKerndEeIKn54Vw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-2.0.0.tgz", + "integrity": "sha512-fbcRrJ9cWJ7/GQIXe8j5HKPpu5TB29jEvpG3H2OZWYlTF3kWoVPixd9wQ9uZNyilyYxqSYxJ4r4WVnAmxNseYA==", "dependencies": { - "resend": "^1.1.0" + "resend": "^2.0.0" }, "peerDependencies": { "nodemailer": "^6.9.3" @@ -5106,237 +5106,18 @@ "@babel/runtime": "^7.13.10" } }, - "node_modules/@react-email/body": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.2.tgz", - "integrity": "sha512-SqZrZdxZlH7viwnrLvrMnVzOKpiofVAtho09bmm2siDzy0VMDGItXRzUPLcpg9vcbVJCHZRCIKoNXqA+PtokzQ==", - "dependencies": { - "react": "18.2.0" - } - }, - "node_modules/@react-email/button": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.9.tgz", - "integrity": "sha512-eYWQ1X4RFlkKYYSPgSrT6rk98wuLOieEAGENrp9j37t1v/1C+jMmBu0UjZvwHsHWdbOMRjbVDFeMI/+MxWKSEg==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/column": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.7.tgz", - "integrity": "sha512-B29wVXyIcuVprgGpLkR23waPh/twlqmugZQsCKk05JlMCQ80/Puv4Lgj4dRsIJzgyTLMwG6xq17+Uxc5iGfuaQ==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/components": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.7.tgz", - "integrity": "sha512-GpRKV8E7EvK9OPf61f5Z8hliB3p0hTot8tslmEUVCTtX7tdL0wM2YEcZiDWU4PJcudJ/QWHJ7Y5wGzNEARcooA==", - "dependencies": { - "@react-email/body": "0.0.2", - "@react-email/button": "0.0.9", - "@react-email/column": "0.0.7", - "@react-email/container": "0.0.8", - "@react-email/font": "0.0.2", - "@react-email/head": "0.0.5", - "@react-email/heading": "0.0.8", - "@react-email/hr": "0.0.5", - "@react-email/html": "0.0.4", - "@react-email/img": "0.0.5", - "@react-email/link": "0.0.5", - "@react-email/preview": "0.0.6", - "@react-email/render": "0.0.7", - "@react-email/row": "0.0.5", - "@react-email/section": "0.0.9", - "@react-email/tailwind": "0.0.8", - "@react-email/text": "0.0.5", - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/container": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.8.tgz", - "integrity": "sha512-MQZQxvTOoLWjJR+Jm689jltm0I/mtZbEaDnwZbNkkHKgccr++wwb9kOKMgXG777Y7tGa1JATAsZpvFYiCITwUg==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/font": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.2.tgz", - "integrity": "sha512-mmkyOCAcbgytE7DfIuOBVG1YVDUZY9rPCor4o7pUEzGJiU2y/TNuV8CgNPSU/VgXeBKL/94QDjB62OrGHlFNMQ==", - "dependencies": { - "react": "18.2.0" - } - }, - "node_modules/@react-email/head": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.5.tgz", - "integrity": "sha512-s84OxJxZMee2z5b1a+RVwY1NOSUNNf1ecjPf6n64aZmMNcNUyn4gOl7RO6xbfBrZko7TigBwsFB1Cgjxtn/ydg==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/heading": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.8.tgz", - "integrity": "sha512-7atATmoHBHTk7hFYFsFFzOIBV3u1zPpsSOWkLBojdjSUdenpk2SbX8GP8/3aBhWl/tuFX9RBGcu1Xes+ZijFLg==", - "dependencies": { - "@radix-ui/react-slot": "1.0.0", - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/heading/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", - "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@react-email/heading/node_modules/@radix-ui/react-slot": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", - "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@react-email/hr": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.5.tgz", - "integrity": "sha512-nwB8GmSdvPlR/bWjDS07yHtgdfJqtvCaPXee3SVUY69YYP7NeDO/VACJlgrS9V2l79bj1lUpH0MJMU6MNAk5FQ==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/html": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.4.tgz", - "integrity": "sha512-7tRYSnudYAWez+NkPWOM8yLZH7EuYFtYdiLPnzpD+pf4cdk16Gz4up531DaIX6dNBbfbyEFpQxhXZxGeJ5ZkfQ==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/img": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.5.tgz", - "integrity": "sha512-9ziFgBfrIAL+DpVlsraFcd2KwsTRyobLpqTnoiBYCcVZGod59xbYkmsmB3CbUosmLwPYg6AeD7Q7e+hCiwkWgg==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/link": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.5.tgz", - "integrity": "sha512-z+QW9f4gXBdyfhl7iYMY3td+rXKeZYK/2AGElEMsxVoywn5D0b6cF8m5w2jbf0U2V3enT+zy9yc1R6AyT59NOg==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/preview": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.6.tgz", - "integrity": "sha512-mXDCc3NGpm/4W7gowBtjsTxYXowLNOLsJsYhIfrsjNJWGlVhVFB9uEHm55LjBLpxSG020g6/8LIrpJU6g22qvg==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@react-email/render": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.7.tgz", - "integrity": "sha512-hMMhxk6TpOcDC5qnKzXPVJoVGEwfm+U5bGOPH+MyTTlx0F02RLQygcATBKsbP7aI/mvkmBAZoFbgPIHop7ovug==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.9.tgz", + "integrity": "sha512-nrim7wiACnaXsGtL7GF6jp3Qmml8J6vAjAH88jkC8lIbfNZaCyuPQHANjyYIXlvQeAbsWADQJFZgOHUqFqjh/A==", "dependencies": { - "html-to-text": "9.0.3", + "html-to-text": "9.0.5", "pretty": "2.0.0", "react": "18.2.0", "react-dom": "18.2.0" }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/row": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.5.tgz", - "integrity": "sha512-dir5l1M7Z/1BQqQkUrKUPIIDPt6ueEf6ScMGoBOcUh+VNNqmnqJE2Q2CH5X3w2uo6a5X7tnVhoJHGa2KTKe8Sw==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/section": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.9.tgz", - "integrity": "sha512-3EbcWJ1jUZrzquWSvXrv8Hbk9V+BGvLcMWQIli4NdIpQlddmlGKUYfXU2mB2d2pf+5ojqkGcFZZ9fWxycB84jQ==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/tailwind": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.8.tgz", - "integrity": "sha512-0BLjD5GpiyBK7YDlaDrjHIpj9eTrrZrMJud3f1UPoCZhS+0S/M8LcR8WMbQsR+8/aLGmiy4F4TGZuRQcsJEsFw==", - "dependencies": { - "html-react-parser": "3.0.9", - "react": "18.2.0", - "react-dom": "18.2.0", - "tw-to-css": "0.0.11" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@react-email/text": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.5.tgz", - "integrity": "sha512-LXhHiaC6oRRsNAfOzJDos4wQA22eIdVJvR6G7uu4QzUvYNOAatDMf89jRQcKGrxX7InkS640v8sHd9jl5ztM5w==", - "dependencies": { - "react": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/@rushstack/eslint-patch": { @@ -5353,12 +5134,12 @@ } }, "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.10.0.tgz", - "integrity": "sha512-gW69MEamZ4wk1OsOq1nG1jcyhXIQcnrsX5JwixVw/9xaiav8TCyjESAruu1Rz9yyInhgBXxkNwMeygKnN2uxNA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", "dependencies": { "domhandler": "^5.0.3", - "selderee": "^0.10.0" + "selderee": "^0.11.0" }, "funding": { "url": "https://ko-fi.com/killymxi" @@ -6766,35 +6547,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-node/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node/node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -8783,14 +8535,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8849,22 +8593,6 @@ "node": ">=12" } }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -11181,57 +10909,25 @@ "node": ">=10" } }, - "node_modules/html-dom-parser": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-3.1.3.tgz", - "integrity": "sha512-fI0yyNlIeSboxU+jnrA4v8qj4+M8SI9/q6AKYdwCY2qki22UtKCDTxvagHniECu7sa5/o2zFRdLleA67035lsA==", - "dependencies": { - "domhandler": "5.0.3", - "htmlparser2": "8.0.1" - } - }, - "node_modules/html-react-parser": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-3.0.9.tgz", - "integrity": "sha512-gOPZmaCMXNYu7Y9+58k2tLhTMXQ+QN8ctNFijzLuBxJaLZ6TsN+tUpN+MhbI+6nGaBCRGT2rpw6y/AqkTFZckg==", - "dependencies": { - "domhandler": "5.0.3", - "html-dom-parser": "3.1.3", - "react-property": "2.0.0", - "style-to-js": "1.1.3" - }, - "peerDependencies": { - "react": "0.14 || 15 || 16 || 17 || 18" - } - }, "node_modules/html-to-text": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.3.tgz", - "integrity": "sha512-hxDF1kVCF2uw4VUJ3vr2doc91pXf2D5ngKcNviSitNkhP9OMOaJkDrFIFL6RMvko7NisWTEiqGpQ9LAxcVok1w==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", "dependencies": { - "@selderee/plugin-htmlparser2": "^0.10.0", - "deepmerge": "^4.2.2", + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", - "htmlparser2": "^8.0.1", - "selderee": "^0.10.0" + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" }, "engines": { "node": ">=14" } }, - "node_modules/html-void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", - "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "node_modules/html-to-text/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -11241,9 +10937,18 @@ ], "dependencies": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "domhandler": "^5.0.3", "domutils": "^3.0.1", - "entities": "^4.3.0" + "entities": "^4.4.0" + } + }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/http-errors": { @@ -14653,12 +14358,12 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, "node_modules/parseley": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.11.0.tgz", - "integrity": "sha512-VfcwXlBWgTF+unPcr7yu3HSSA6QUdDaDnrHcytVfj5Z8azAyKBDrYnSIfeSxlrEayndNcLmrXzg+Vxbo6DWRXQ==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", "dependencies": { "leac": "^0.6.0", - "peberminta": "^0.8.0" + "peberminta": "^0.9.0" }, "funding": { "url": "https://ko-fi.com/killymxi" @@ -14768,9 +14473,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/peberminta": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.8.0.tgz", - "integrity": "sha512-YYEs+eauIjDH5nUEGi18EohWE0nV2QbGTqmxQcqgZ/0g+laPCQmuIqq7EBLVi9uim9zMgfJv0QBZEnQ3uHw/Tw==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", "funding": { "url": "https://ko-fi.com/killymxi" } @@ -15968,20 +15673,6 @@ "node": ">=12" } }, - "node_modules/react-email/node_modules/@react-email/render": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.6.tgz", - "integrity": "sha512-6zs7WZbd37TcPT1OmMPH/kcBpv0QSi+k3om7LyDnbdIcrbwOO/OstVwUaa/6zgvDvnq9Y2wOosbru7j5kUrW9A==", - "dependencies": { - "html-to-text": "9.0.3", - "pretty": "2.0.0", - "react": "18.2.0", - "react-dom": "18.2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/react-email/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -16105,11 +15796,6 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, - "node_modules/react-property": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", - "integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==" - }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", @@ -16688,28 +16374,16 @@ } }, "node_modules/resend": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz", - "integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-2.0.0.tgz", + "integrity": "sha512-jAh0DN84ZjjmzGM2vMjJ1hphPBg1mG98dzopF7kJzmin62v8ESg4og2iCKWdkAboGOT2SeO5exbr/8Xh8gLddw==", "dependencies": { - "@react-email/render": "0.0.7", - "type-fest": "3.13.0" + "@react-email/render": "0.0.9" }, "engines": { "node": ">=18" } }, - "node_modules/resend/node_modules/type-fest": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz", - "integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -17017,11 +16691,11 @@ } }, "node_modules/selderee": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.10.0.tgz", - "integrity": "sha512-DEL/RW/f4qLw/NrVg97xKaEBC8IpzIG2fvxnzCp3Z4yk4jQ3MXom+Imav9wApjxX2dfS3eW7x0DXafJr85i39A==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", "dependencies": { - "parseley": "^0.11.0" + "parseley": "^0.12.0" }, "funding": { "url": "https://ko-fi.com/killymxi" @@ -17685,22 +17359,6 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, - "node_modules/style-to-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz", - "integrity": "sha512-zKI5gN/zb7LS/Vm0eUwjmjrXWw8IMtyA8aPBJZdYiQTXj4+wQ3IucOLIOnF7zCHxvW8UhIGh/uZh/t9zEHXNTQ==", - "dependencies": { - "style-to-object": "0.4.1" - } - }, - "node_modules/style-to-js/node_modules/style-to-object": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", - "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, "node_modules/style-to-object": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", @@ -18409,199 +18067,6 @@ "win32" ] }, - "node_modules/tw-to-css": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/tw-to-css/-/tw-to-css-0.0.11.tgz", - "integrity": "sha512-uIJuEBIwyHzZg9xyGyEgDWHIkbAwEC4bhEHQ4THPuN5SToR7Zlhes5ffMjqtrv+WdtTmudTHTdc9VwUldy0FxQ==", - "dependencies": { - "postcss": "8.4.21", - "postcss-css-variables": "0.18.0", - "tailwindcss": "3.2.7" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/tw-to-css/node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "node_modules/tw-to-css/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tw-to-css/node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/tw-to-css/node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/tw-to-css/node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/tw-to-css/node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/tw-to-css/node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/tw-to-css/node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tw-to-css/node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tw-to-css/node_modules/tailwindcss": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", - "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==", - "dependencies": { - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/tw-to-css/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, "node_modules/tween-functions": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", @@ -19608,11 +19073,11 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@documenso/nodemailer-resend": "1.0.0", - "@react-email/components": "^0.0.7", + "@documenso/nodemailer-resend": "2.0.0", + "@react-email/components": "^0.0.11", "nodemailer": "^6.9.3", - "react-email": "^1.9.4", - "resend": "^1.1.0" + "react-email": "^1.9.5", + "resend": "^2.0.0" }, "devDependencies": { "@documenso/tailwind-config": "*", @@ -19621,6 +19086,298 @@ "tsup": "^7.1.0" } }, + "packages/email/node_modules/@react-email/body": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.4.tgz", + "integrity": "sha512-NmHOumdmyjWvOXomqhQt06KbgRxhHrVznxQp/oWiPWes8nAJo2Y4L27aPHR9nTcs7JF7NmcJe9YSN42pswK+GQ==", + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/button": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.11.tgz", + "integrity": "sha512-mB5ySfZifwE5ybtIWwXGbmKk1uKkH4655gftL4+mMxZAZCkINVa2KXTi5pO+xZhMtJI9xtAsikOrOEU1gTDoww==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/column": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.8.tgz", + "integrity": "sha512-blChqGU8e/L6KZiB5EPww8bkZfdyHDuS0vKIvU+iS14uK+xfAw+5P5CU9BYXccEuJh2Gftfngu1bWMFp2Sc6ag==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/components": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.11.tgz", + "integrity": "sha512-wj9Sra/AGQvadb3ZABz44ll9Fb9FvXPEmODXRWbNRSc8pJTFGWorrsm4M/yj8dnewd4HtnbLkV1eDOvuiLAVLA==", + "dependencies": { + "@react-email/body": "0.0.4", + "@react-email/button": "0.0.11", + "@react-email/column": "0.0.8", + "@react-email/container": "0.0.10", + "@react-email/font": "0.0.4", + "@react-email/head": "0.0.6", + "@react-email/heading": "0.0.9", + "@react-email/hr": "0.0.6", + "@react-email/html": "0.0.6", + "@react-email/img": "0.0.6", + "@react-email/link": "0.0.6", + "@react-email/preview": "0.0.7", + "@react-email/render": "0.0.9", + "@react-email/row": "0.0.6", + "@react-email/section": "0.0.10", + "@react-email/tailwind": "0.0.12", + "@react-email/text": "0.0.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/container": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.10.tgz", + "integrity": "sha512-goishY7ocq+lord0043/LZK268bqvMFW/sxpUt/dSCPJyrrZZNCbpW2t8w8HztU38cYj0qGQLxO5Qvpn/RER3w==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/font": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.4.tgz", + "integrity": "sha512-rN/pFlAcDNmfYFxpufT/rFRrM5KYBJM4nTA2uylTehlVOro6fb/q6n0zUwLF6OmQ4QIuRbqdEy7DI9mmJiNHxA==", + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/head": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.6.tgz", + "integrity": "sha512-9BrBDalb34nBOmmQVQc7/pjJotcuAeC3rhBl4G88Ohiipuv15vPIKqwy8vPJcFNi4l7yGlitfG3EESIjkLkoIw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/heading": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.9.tgz", + "integrity": "sha512-xzkcGlm+/aFrNlJZBKzxRKkRYJ2cRx92IqmSKAuGnwuKQ/uMKomXzPsHPu3Dclmnhn3wVKj4uprkgQOoxP6uXQ==", + "dependencies": { + "@radix-ui/react-slot": "1.0.2", + "react": "18.2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/email/node_modules/@react-email/hr": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.6.tgz", + "integrity": "sha512-W+wINBz7z7BRv3i9GS+QoJBae1PESNhv6ZY6eLnEpqtBI/2++suuRNJOU/KpZzE6pykeTp6I/Z7UcL0LEYKgyg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/html": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.6.tgz", + "integrity": "sha512-8Fo20VOqxqc087gGEPjT8uos06fTXIC8NSoiJxpiwAkwiKtQnQH/jOdoLv6XaWh5Zt2clj1uokaoklnaM5rY1w==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/img": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.6.tgz", + "integrity": "sha512-Wd7xKI3b1Jvb2ZEHyVpJ9D98u0GHrRl+578b8LV24PavM/65V61Q5LN5Fr9sAhj+4VGqnHDIVeXIYEzVbWaa3Q==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/link": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.6.tgz", + "integrity": "sha512-bYYHroWGS//nDl9yhh8V6K2BrNwAsyX7N/XClSCRku3x56NrZ6D0nBKWewYDPlJ9rW9TIaJm1jDYtO9XBzLlkQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/preview": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.7.tgz", + "integrity": "sha512-YLfIwHdexPi8IgP1pSuVXdAmKzMQ8ctCCLEjkMttT2vkSFqT6m/e6UFWK2l30rKm2dDsLvQyEvo923mPXjnNzg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/row": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.6.tgz", + "integrity": "sha512-msJ2TnDJNwpgDfDzUO63CvhusJHeaGLMM+8Zz86VPvxzwe/DkT7N48QKRWRCkt8urxVz5U+EgivORA9Dum9p3Q==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/section": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.10.tgz", + "integrity": "sha512-x9B2KYFqj+d8I1fK9bgeVm/3mLE4Qgn4mm/GbDtcJeSzKU/G7bTb7/3+BMDk9SARPGkg5XAuZm1XgcqQQutt2A==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/tailwind": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.12.tgz", + "integrity": "sha512-s8Ch7GL30qRKScn9NWwItMqxjtzbyUtCnXfC6sL2YTVtulbfvZZ06W+aA0S6f7fdrVlOOlQzZuK/sVaQCHhcSw==", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "tw-to-css": "0.0.12" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/@react-email/text": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.6.tgz", + "integrity": "sha512-PDUTAD1PjlzXFOIUrR1zuV2xxguL62yne5YLcn1k+u/dVUyzn6iU/5lFShxCfzuh3QDWCf4+JRNnXN9rmV6jzw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "18.2.0" + } + }, + "packages/email/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "packages/email/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "packages/email/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "packages/email/node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "packages/email/node_modules/tailwindcss": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "packages/email/node_modules/tw-to-css": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/tw-to-css/-/tw-to-css-0.0.12.tgz", + "integrity": "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw==", + "dependencies": { + "postcss": "8.4.31", + "postcss-css-variables": "0.18.0", + "tailwindcss": "3.3.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "packages/eslint-config": { "name": "@documenso/eslint-config", "version": "0.0.0", diff --git a/package.json b/package.json index 1b4690e18..d21af733e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "packages/*" ], "dependencies": { - "react-hotkeys-hook": "^4.4.1", - "recharts": "^2.7.2" + "recharts": "^2.7.2", + "react-hotkeys-hook": "^4.4.1" } } diff --git a/packages/email/package.json b/packages/email/package.json index 4b23512ce..6366c67ed 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -17,11 +17,11 @@ "worker:test": "tsup worker/index.ts --format esm" }, "dependencies": { - "@documenso/nodemailer-resend": "1.0.0", - "@react-email/components": "^0.0.7", + "@documenso/nodemailer-resend": "2.0.0", + "@react-email/components": "^0.0.11", "nodemailer": "^6.9.3", - "react-email": "^1.9.4", - "resend": "^1.1.0" + "react-email": "^1.9.5", + "resend": "^2.0.0" }, "devDependencies": { "@documenso/tailwind-config": "*", diff --git a/packages/email/template-components/template-confirmation-email.tsx b/packages/email/template-components/template-confirmation-email.tsx new file mode 100644 index 000000000..e46582f54 --- /dev/null +++ b/packages/email/template-components/template-confirmation-email.tsx @@ -0,0 +1,52 @@ +import { Button, Section, Tailwind, Text } from '@react-email/components'; + +import * as config from '@documenso/tailwind-config'; + +import { TemplateDocumentImage } from './template-document-image'; + +export type TemplateConfirmationEmailProps = { + confirmationLink: string; + assetBaseUrl: string; +}; + +export const TemplateConfirmationEmail = ({ + confirmationLink, + assetBaseUrl, +}: TemplateConfirmationEmailProps) => { + return ( + + + +
+ + Welcome to Documenso! + + + + Before you get started, please confirm your email address by clicking the button below: + + +
+ + + You can also copy and paste this link into your browser: {confirmationLink} (link + expires in 1 hour) + +
+
+
+ ); +}; diff --git a/packages/email/templates/confirm-email.tsx b/packages/email/templates/confirm-email.tsx new file mode 100644 index 000000000..5e917f0a3 --- /dev/null +++ b/packages/email/templates/confirm-email.tsx @@ -0,0 +1,69 @@ +import { + Body, + Container, + Head, + Html, + Img, + Preview, + Section, + Tailwind, +} from '@react-email/components'; + +import config from '@documenso/tailwind-config'; + +import { + TemplateConfirmationEmail, + TemplateConfirmationEmailProps, +} from '../template-components/template-confirmation-email'; +import { TemplateFooter } from '../template-components/template-footer'; + +export const ConfirmEmailTemplate = ({ + confirmationLink, + assetBaseUrl, +}: TemplateConfirmationEmailProps) => { + const previewText = `Please confirm your email address`; + + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + + + {previewText} + + +
+ +
+ Documenso Logo + + +
+
+
+ + + + +
+ +
+ + ); +}; diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts index cd7692e52..216962293 100644 --- a/packages/lib/next-auth/auth-options.ts +++ b/packages/lib/next-auth/auth-options.ts @@ -88,6 +88,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = { merged.id = retrieved.id; merged.name = retrieved.name; merged.email = retrieved.email; + merged.emailVerified = retrieved.emailVerified; } if ( @@ -112,6 +113,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = { name: merged.name, email: merged.email, lastSignedIn: merged.lastSignedIn, + emailVerified: merged.emailVerified, }; }, @@ -123,6 +125,8 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = { id: Number(token.id), name: token.name, email: token.email, + emailVerified: + typeof token.emailVerified === 'string' ? new Date(token.emailVerified) : null, }, } satisfies Session; } diff --git a/packages/lib/server-only/auth/send-confirmation-email.ts b/packages/lib/server-only/auth/send-confirmation-email.ts new file mode 100644 index 000000000..7defdb1bd --- /dev/null +++ b/packages/lib/server-only/auth/send-confirmation-email.ts @@ -0,0 +1,56 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { ConfirmEmailTemplate } from '@documenso/email/templates/confirm-email'; +import { prisma } from '@documenso/prisma'; + +export interface SendConfirmationEmailProps { + userId: number; +} + +export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + include: { + VerificationToken: { + orderBy: { + createdAt: 'desc', + }, + take: 1, + }, + }, + }); + + const [verificationToken] = user.VerificationToken; + + if (!verificationToken?.token) { + throw new Error('Verification token not found for the user'); + } + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + const confirmationLink = `${assetBaseUrl}/verify-email/${verificationToken.token}`; + const senderName = process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso'; + const senderAdress = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com'; + + const confirmationTemplate = createElement(ConfirmEmailTemplate, { + assetBaseUrl, + confirmationLink, + }); + + return mailer.sendMail({ + to: { + address: user.email, + name: user.name || '', + }, + from: { + name: senderName, + address: senderAdress, + }, + subject: 'Please confirm your email', + html: render(confirmationTemplate), + text: render(confirmationTemplate, { plainText: true }), + }); +}; diff --git a/packages/lib/server-only/user/generate-confirmation-token.ts b/packages/lib/server-only/user/generate-confirmation-token.ts new file mode 100644 index 000000000..5676890ec --- /dev/null +++ b/packages/lib/server-only/user/generate-confirmation-token.ts @@ -0,0 +1,41 @@ +import crypto from 'crypto'; + +import { prisma } from '@documenso/prisma'; + +import { ONE_HOUR } from '../../constants/time'; +import { sendConfirmationEmail } from '../auth/send-confirmation-email'; + +const IDENTIFIER = 'confirmation-email'; + +export const generateConfirmationToken = async ({ email }: { email: string }) => { + const token = crypto.randomBytes(20).toString('hex'); + + const user = await prisma.user.findFirst({ + where: { + email: email, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + const createdToken = await prisma.verificationToken.create({ + data: { + identifier: IDENTIFIER, + token: token, + expires: new Date(Date.now() + ONE_HOUR), + user: { + connect: { + id: user.id, + }, + }, + }, + }); + + if (!createdToken) { + throw new Error(`Failed to create the verification token`); + } + + return sendConfirmationEmail({ userId: user.id }); +}; diff --git a/packages/lib/server-only/user/send-confirmation-token.ts b/packages/lib/server-only/user/send-confirmation-token.ts new file mode 100644 index 000000000..5206d202e --- /dev/null +++ b/packages/lib/server-only/user/send-confirmation-token.ts @@ -0,0 +1,41 @@ +import crypto from 'crypto'; + +import { prisma } from '@documenso/prisma'; + +import { ONE_HOUR } from '../../constants/time'; +import { sendConfirmationEmail } from '../auth/send-confirmation-email'; + +const IDENTIFIER = 'confirmation-email'; + +export const sendConfirmationToken = async ({ email }: { email: string }) => { + const token = crypto.randomBytes(20).toString('hex'); + + const user = await prisma.user.findFirst({ + where: { + email: email, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + const createdToken = await prisma.verificationToken.create({ + data: { + identifier: IDENTIFIER, + token: token, + expires: new Date(Date.now() + ONE_HOUR), + user: { + connect: { + id: user.id, + }, + }, + }, + }); + + if (!createdToken) { + throw new Error(`Failed to create the verification token`); + } + + return sendConfirmationEmail({ userId: user.id }); +}; diff --git a/packages/lib/server-only/user/verify-email.ts b/packages/lib/server-only/user/verify-email.ts new file mode 100644 index 000000000..e954df1f8 --- /dev/null +++ b/packages/lib/server-only/user/verify-email.ts @@ -0,0 +1,70 @@ +import { DateTime } from 'luxon'; + +import { prisma } from '@documenso/prisma'; + +import { sendConfirmationToken } from './send-confirmation-token'; + +export type VerifyEmailProps = { + token: string; +}; + +export const verifyEmail = async ({ token }: VerifyEmailProps) => { + const verificationToken = await prisma.verificationToken.findFirst({ + include: { + user: true, + }, + where: { + token, + }, + }); + + if (!verificationToken) { + return null; + } + + // check if the token is valid or expired + const valid = verificationToken.expires > new Date(); + + if (!valid) { + const mostRecentToken = await prisma.verificationToken.findFirst({ + where: { + userId: verificationToken.userId, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + // If there isn't a recent token or it's older than 1 hour, send a new token + if ( + !mostRecentToken || + DateTime.now().minus({ hours: 1 }).toJSDate() > mostRecentToken.createdAt + ) { + await sendConfirmationToken({ email: verificationToken.user.email }); + } + + return valid; + } + + const [updatedUser, deletedToken] = await prisma.$transaction([ + prisma.user.update({ + where: { + id: verificationToken.userId, + }, + data: { + emailVerified: new Date(), + }, + }), + prisma.verificationToken.deleteMany({ + where: { + userId: verificationToken.userId, + }, + }), + ]); + + if (!updatedUser || !deletedToken) { + throw new Error('Something went wrong while verifying your email. Please try again.'); + } + + return !!updatedUser && !!deletedToken; +}; diff --git a/packages/prisma/migrations/20231025074705_add_email_confirmation_registration/migration.sql b/packages/prisma/migrations/20231025074705_add_email_confirmation_registration/migration.sql new file mode 100644 index 000000000..95e64a744 --- /dev/null +++ b/packages/prisma/migrations/20231025074705_add_email_confirmation_registration/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "VerificationToken" ( + "id" SERIAL NOT NULL, + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + + CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- AddForeignKey +ALTER TABLE "VerificationToken" ADD CONSTRAINT "VerificationToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231031072857_verify_existing_users/migration.sql b/packages/prisma/migrations/20231031072857_verify_existing_users/migration.sql new file mode 100644 index 000000000..5b082c233 --- /dev/null +++ b/packages/prisma/migrations/20231031072857_verify_existing_users/migration.sql @@ -0,0 +1,3 @@ +UPDATE "User" +SET "emailVerified" = CURRENT_TIMESTAMP +WHERE "emailVerified" IS NULL; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 8cf4152c4..02807e4a0 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -36,7 +36,8 @@ model User { Document Document[] Subscription Subscription? PasswordResetToken PasswordResetToken[] - + VerificationToken VerificationToken[] + @@index([email]) } @@ -49,6 +50,16 @@ model PasswordResetToken { User User @relation(fields: [userId], references: [id]) } +model VerificationToken { + id Int @id @default(autoincrement()) + identifier String + token String @unique + expires DateTime + createdAt DateTime @default(now()) + userId Int + user User @relation(fields: [userId], references: [id]) +} + enum SubscriptionStatus { ACTIVE PAST_DUE diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts index f66f44325..dfabd9da9 100644 --- a/packages/trpc/server/auth-router/router.ts +++ b/packages/trpc/server/auth-router/router.ts @@ -1,6 +1,7 @@ import { TRPCError } from '@trpc/server'; import { createUser } from '@documenso/lib/server-only/user/create-user'; +import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token'; import { procedure, router } from '../trpc'; import { ZSignUpMutationSchema } from './schema'; @@ -10,7 +11,11 @@ export const authRouter = router({ try { const { name, email, password, signature } = input; - return await createUser({ name, email, password, signature }); + const user = await createUser({ name, email, password, signature }); + + await sendConfirmationToken({ email: user.email }); + + return user; } catch (err) { let message = 'We were unable to create your account. Please review the information you provided and try again.'; diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 0f6636650..4dcf4ca93 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -3,11 +3,13 @@ import { TRPCError } from '@trpc/server'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id'; import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; +import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-confirmation-token'; import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updateProfile } from '@documenso/lib/server-only/user/update-profile'; import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc'; import { + ZConfirmEmailMutationSchema, ZForgotPasswordFormSchema, ZResetPasswordFormSchema, ZRetrieveUserByIdQuerySchema, @@ -110,4 +112,25 @@ export const profileRouter = router({ }); } }), + + sendConfirmationEmail: procedure + .input(ZConfirmEmailMutationSchema) + .mutation(async ({ input }) => { + try { + const { email } = input; + + return sendConfirmationToken({ email }); + } catch (err) { + let message = 'We were unable to send a confirmation email. Please try again.'; + + if (err instanceof Error) { + message = err.message; + } + + throw new TRPCError({ + code: 'BAD_REQUEST', + message, + }); + } + }), }); diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index 44a8a451c..ef9ca2a14 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -23,8 +23,13 @@ export const ZResetPasswordFormSchema = z.object({ token: z.string().min(1), }); +export const ZConfirmEmailMutationSchema = z.object({ + email: z.string().email().min(1), +}); + export type TRetrieveUserByIdQuerySchema = z.infer; export type TUpdateProfileMutationSchema = z.infer; export type TUpdatePasswordMutationSchema = z.infer; export type TForgotPasswordFormSchema = z.infer; export type TResetPasswordFormSchema = z.infer; +export type TConfirmEmailMutationSchema = z.infer;