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.
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
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}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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;