From 4aefb809894c3ff7f98eb572b458df2edb6c9d40 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 16 Jan 2024 14:25:05 +0200
Subject: [PATCH 01/14] feat: restrict app access for unverified users
---
.../unverified-account/page.tsx | 80 +++++++++++++++++++
apps/web/src/components/forms/signin.tsx | 17 ++++
apps/web/src/components/forms/signup.tsx | 14 ++--
packages/lib/next-auth/auth-options.ts | 12 +++
packages/lib/next-auth/error-codes.ts | 1 +
.../lib/server-only/user/get-user-by-email.ts | 3 +
.../user/get-user-by-verification-token.ts | 17 ++++
packages/trpc/server/profile-router/router.ts | 34 ++++++++
packages/trpc/server/profile-router/schema.ts | 8 ++
9 files changed, 181 insertions(+), 5 deletions(-)
create mode 100644 apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
create mode 100644 packages/lib/server-only/user/get-user-by-verification-token.ts
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
new file mode 100644
index 000000000..7a0a9c78d
--- /dev/null
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { useState } from 'react';
+
+import { useSearchParams } from 'next/navigation';
+
+import { Mails } 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 { useToast } from '@documenso/ui/primitives/use-toast';
+
+const RESEND_CONFIRMATION_EMAIL_TIMEOUT = 20 * ONE_SECOND;
+
+export default function UnverifiedAccount() {
+ const [isButtonDisabled, setIsButtonDisabled] = useState(false);
+ const searchParams = useSearchParams();
+ const { toast } = useToast();
+
+ const token = searchParams?.get('t') ?? '';
+
+ const { data: { email } = {} } = trpc.profile.getUserFromVerificationToken.useQuery({ token });
+
+ const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
+
+ const onResendConfirmationEmail = async () => {
+ if (!email) {
+ toast({
+ title: 'Unable to send confirmation email',
+ description: 'Something went wrong while sending the confirmation email. Please try again.',
+ variant: 'destructive',
+ });
+
+ return;
+ }
+
+ try {
+ setIsButtonDisabled(true);
+
+ await sendConfirmationEmail({ email: email });
+
+ toast({
+ title: 'Success',
+ description: 'Verification email sent successfully.',
+ duration: 5000,
+ });
+
+ 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',
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
Confirm email
+
+
+ To gain full access to your account and unlock all its features, please confirm your email
+ address by clicking on the link sent to your email address.
+
+
+
+ Resend email
+
+
+
+ );
+}
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
index 4e671a569..2924080b0 100644
--- a/apps/web/src/components/forms/signin.tsx
+++ b/apps/web/src/components/forms/signin.tsx
@@ -2,6 +2,8 @@
import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+
import { zodResolver } from '@hookform/resolvers/zod';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
@@ -9,6 +11,7 @@ import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
+import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
@@ -31,6 +34,8 @@ const ERROR_MESSAGES: Partial> = {
'This account appears to be using a social login method, please sign in using that method',
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
+ [ErrorCode.UNVERIFIED_EMAIL]:
+ 'This account has not been verified. Please verify your account before signing in.',
};
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
@@ -54,6 +59,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
const { toast } = useToast();
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
useState(false);
+ const router = useRouter();
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
'totp' | 'backup'
@@ -69,6 +75,8 @@ export const SignInForm = ({ className }: SignInFormProps) => {
resolver: zodResolver(ZSignInFormSchema),
});
+ const { mutateAsync: getUser } = trpc.profile.getUserByEmail.useMutation();
+
const isSubmitting = form.formState.isSubmitting;
const onCloseTwoFactorAuthenticationDialog = () => {
@@ -122,6 +130,15 @@ export const SignInForm = ({ className }: SignInFormProps) => {
const errorMessage = ERROR_MESSAGES[result.error];
+ if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
+ const user = await getUser({ email });
+ const token = user?.VerificationToken[user.VerificationToken.length - 1].token;
+
+ router.push(`/unverified-account?t=${token}`);
+
+ return;
+ }
+
toast({
variant: 'destructive',
title: 'Unable to sign in',
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index b91b4a9fd..526836ca7 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -1,7 +1,8 @@
'use client';
+import { useRouter } from 'next/navigation';
+
import { zodResolver } from '@hookform/resolvers/zod';
-import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -42,6 +43,7 @@ export type SignUpFormProps = {
export const SignUpForm = ({ className }: SignUpFormProps) => {
const { toast } = useToast();
const analytics = useAnalytics();
+ const router = useRouter();
const form = useForm({
values: {
@@ -61,10 +63,12 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
try {
await signup({ name, email, password, signature });
- await signIn('credentials', {
- email,
- password,
- callbackUrl: '/',
+ router.push('/signin');
+
+ toast({
+ title: 'Registration Successful',
+ description: 'You have successfully registered. Please sign in to continue.',
+ duration: 5000,
});
analytics.capture('App: User Sign Up', {
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index 3b9492807..4c529d113 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -10,6 +10,7 @@ import GoogleProvider from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma';
+import { ONE_DAY } from '../constants/time';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
@@ -69,6 +70,17 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
}
+ const userCreationDate = user?.createdAt;
+ const createdWithinLast72Hours = userCreationDate > new Date(Date.now() - ONE_DAY * 3);
+
+ /*
+ avoid messing with the users who signed up before the email verification requirement
+ the error is thrown only if the user doesn't have a verified email and the account was created within the last 72 hours
+ */
+ if (!user.emailVerified && createdWithinLast72Hours) {
+ throw new Error(ErrorCode.UNVERIFIED_EMAIL);
+ }
+
return {
id: Number(user.id),
email: user.email,
diff --git a/packages/lib/next-auth/error-codes.ts b/packages/lib/next-auth/error-codes.ts
index c3dfafece..6e1b7488b 100644
--- a/packages/lib/next-auth/error-codes.ts
+++ b/packages/lib/next-auth/error-codes.ts
@@ -19,4 +19,5 @@ export const ErrorCode = {
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
+ UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
} as const;
diff --git a/packages/lib/server-only/user/get-user-by-email.ts b/packages/lib/server-only/user/get-user-by-email.ts
index 0a2ef8d16..8c61202a2 100644
--- a/packages/lib/server-only/user/get-user-by-email.ts
+++ b/packages/lib/server-only/user/get-user-by-email.ts
@@ -9,5 +9,8 @@ export const getUserByEmail = async ({ email }: GetUserByEmailOptions) => {
where: {
email: email.toLowerCase(),
},
+ include: {
+ VerificationToken: true,
+ },
});
};
diff --git a/packages/lib/server-only/user/get-user-by-verification-token.ts b/packages/lib/server-only/user/get-user-by-verification-token.ts
new file mode 100644
index 000000000..b33506d6e
--- /dev/null
+++ b/packages/lib/server-only/user/get-user-by-verification-token.ts
@@ -0,0 +1,17 @@
+import { prisma } from '@documenso/prisma';
+
+export interface GetUserByVerificationTokenOptions {
+ token: string;
+}
+
+export const getUserByVerificationToken = async ({ token }: GetUserByVerificationTokenOptions) => {
+ return await prisma.user.findFirstOrThrow({
+ where: {
+ VerificationToken: {
+ some: {
+ token,
+ },
+ },
+ },
+ });
+};
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 4dcf4ca93..79c67ed0c 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,7 +1,9 @@
import { TRPCError } from '@trpc/server';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
+import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
+import { getUserByVerificationToken } from '@documenso/lib/server-only/user/get-user-by-verification-token';
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';
@@ -12,7 +14,9 @@ import {
ZConfirmEmailMutationSchema,
ZForgotPasswordFormSchema,
ZResetPasswordFormSchema,
+ ZRetrieveUserByEmailMutationSchema,
ZRetrieveUserByIdQuerySchema,
+ ZRetrieveUserByVerificationTokenQuerySchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
@@ -31,6 +35,36 @@ export const profileRouter = router({
}
}),
+ getUserByEmail: procedure
+ .input(ZRetrieveUserByEmailMutationSchema)
+ .mutation(async ({ input }) => {
+ try {
+ const { email } = input;
+
+ return await getUserByEmail({ email });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to retrieve the specified account. Please try again.',
+ });
+ }
+ }),
+
+ getUserFromVerificationToken: procedure
+ .input(ZRetrieveUserByVerificationTokenQuerySchema)
+ .query(async ({ input }) => {
+ try {
+ const { token } = input;
+
+ return await getUserByVerificationToken({ token });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to retrieve the specified account. Please try again.',
+ });
+ }
+ }),
+
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index ef9ca2a14..671756e94 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -4,6 +4,14 @@ export const ZRetrieveUserByIdQuerySchema = z.object({
id: z.number().min(1),
});
+export const ZRetrieveUserByEmailMutationSchema = z.object({
+ email: z.string().email().min(1),
+});
+
+export const ZRetrieveUserByVerificationTokenQuerySchema = z.object({
+ token: z.string().min(1),
+});
+
export const ZUpdateProfileMutationSchema = z.object({
name: z.string().min(1),
signature: z.string(),
From 49ecfc1a2cf01e3bcdf195819657b463faf7e890 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Thu, 25 Jan 2024 15:42:40 +0200
Subject: [PATCH 02/14] chore: refactor
---
.../unverified-account/page.tsx | 16 +++-------------
apps/web/src/components/forms/signin.tsx | 9 ++++-----
apps/web/src/components/forms/signup.tsx | 5 ++++-
packages/lib/next-auth/auth-options.ts | 10 +---------
.../lib/server-only/user/get-user-by-email.ts | 3 ---
.../user/get-user-by-verification-token.ts | 17 -----------------
packages/trpc/server/profile-router/router.ts | 19 +------------------
7 files changed, 13 insertions(+), 66 deletions(-)
delete mode 100644 packages/lib/server-only/user/get-user-by-verification-token.ts
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
index 7a0a9c78d..456971a9f 100644
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -20,25 +20,15 @@ export default function UnverifiedAccount() {
const token = searchParams?.get('t') ?? '';
- const { data: { email } = {} } = trpc.profile.getUserFromVerificationToken.useQuery({ token });
-
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
const onResendConfirmationEmail = async () => {
- if (!email) {
- toast({
- title: 'Unable to send confirmation email',
- description: 'Something went wrong while sending the confirmation email. Please try again.',
- variant: 'destructive',
- });
-
- return;
- }
-
try {
setIsButtonDisabled(true);
- await sendConfirmationEmail({ email: email });
+ // TODO: decrypt email and send it
+
+ await sendConfirmationEmail({ email: token ?? '' });
toast({
title: 'Success',
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
index c79021396..4e3701c84 100644
--- a/apps/web/src/components/forms/signin.tsx
+++ b/apps/web/src/components/forms/signin.tsx
@@ -62,6 +62,8 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
useState(false);
const router = useRouter();
+ const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
+
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
'totp' | 'backup'
>('totp');
@@ -76,8 +78,6 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
resolver: zodResolver(ZSignInFormSchema),
});
- const { mutateAsync: getUser } = trpc.profile.getUserByEmail.useMutation();
-
const isSubmitting = form.formState.isSubmitting;
const onCloseTwoFactorAuthenticationDialog = () => {
@@ -132,10 +132,9 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
const errorMessage = ERROR_MESSAGES[result.error];
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
- const user = await getUser({ email });
- const token = user?.VerificationToken[user.VerificationToken.length - 1].token;
+ const encryptedEmail = await encryptSecondaryData({ data: email });
- router.push(`/unverified-account?t=${token}`);
+ router.push(`/unverified-account?t=${encryptedEmail}`);
return;
}
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index 6258dcdee..190084226 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -62,12 +62,15 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
+ const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
await signup({ name, email, password, signature });
- router.push('/signin');
+ const encryptedEmail = await encryptSecondaryData({ data: email });
+
+ router.push(`/unverified-account?t=${encryptedEmail}`);
toast({
title: 'Registration Successful',
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index ed4aeaf44..37f1ed864 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -11,7 +11,6 @@ import GoogleProvider from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma';
import { IdentityProvider } from '@documenso/prisma/client';
-import { ONE_DAY } from '../constants/time';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
@@ -71,14 +70,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
}
- const userCreationDate = user?.createdAt;
- const createdWithinLast72Hours = userCreationDate > new Date(Date.now() - ONE_DAY * 3);
-
- /*
- avoid messing with the users who signed up before the email verification requirement
- the error is thrown only if the user doesn't have a verified email and the account was created within the last 72 hours
- */
- if (!user.emailVerified && createdWithinLast72Hours) {
+ if (!user.emailVerified) {
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
}
diff --git a/packages/lib/server-only/user/get-user-by-email.ts b/packages/lib/server-only/user/get-user-by-email.ts
index 8c61202a2..0a2ef8d16 100644
--- a/packages/lib/server-only/user/get-user-by-email.ts
+++ b/packages/lib/server-only/user/get-user-by-email.ts
@@ -9,8 +9,5 @@ export const getUserByEmail = async ({ email }: GetUserByEmailOptions) => {
where: {
email: email.toLowerCase(),
},
- include: {
- VerificationToken: true,
- },
});
};
diff --git a/packages/lib/server-only/user/get-user-by-verification-token.ts b/packages/lib/server-only/user/get-user-by-verification-token.ts
deleted file mode 100644
index b33506d6e..000000000
--- a/packages/lib/server-only/user/get-user-by-verification-token.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { prisma } from '@documenso/prisma';
-
-export interface GetUserByVerificationTokenOptions {
- token: string;
-}
-
-export const getUserByVerificationToken = async ({ token }: GetUserByVerificationTokenOptions) => {
- return await prisma.user.findFirstOrThrow({
- where: {
- VerificationToken: {
- some: {
- token,
- },
- },
- },
- });
-};
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 79c67ed0c..09ee0351f 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -3,7 +3,6 @@ import { TRPCError } from '@trpc/server';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
-import { getUserByVerificationToken } from '@documenso/lib/server-only/user/get-user-by-verification-token';
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';
@@ -16,7 +15,6 @@ import {
ZResetPasswordFormSchema,
ZRetrieveUserByEmailMutationSchema,
ZRetrieveUserByIdQuerySchema,
- ZRetrieveUserByVerificationTokenQuerySchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
@@ -50,21 +48,6 @@ export const profileRouter = router({
}
}),
- getUserFromVerificationToken: procedure
- .input(ZRetrieveUserByVerificationTokenQuerySchema)
- .query(async ({ input }) => {
- try {
- const { token } = input;
-
- return await getUserByVerificationToken({ token });
- } catch (err) {
- throw new TRPCError({
- code: 'BAD_REQUEST',
- message: 'We were unable to retrieve the specified account. Please try again.',
- });
- }
- }),
-
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
@@ -153,7 +136,7 @@ export const profileRouter = router({
try {
const { email } = input;
- return sendConfirmationToken({ email });
+ return await sendConfirmationToken({ email });
} catch (err) {
let message = 'We were unable to send a confirmation email. Please try again.';
From 311c8da8fc8ad5ded8f8ed11146a517e740f07b5 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Thu, 25 Jan 2024 17:24:37 +0200
Subject: [PATCH 03/14] chore: encrypt and decrypt email addr
---
.../src/app/(unauthenticated)/unverified-account/page.tsx | 6 ++----
packages/trpc/server/profile-router/router.ts | 5 ++++-
packages/trpc/server/profile-router/schema.ts | 4 ++--
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
index 456971a9f..5199249e0 100644
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -18,7 +18,7 @@ export default function UnverifiedAccount() {
const searchParams = useSearchParams();
const { toast } = useToast();
- const token = searchParams?.get('t') ?? '';
+ const encryptedEmail = searchParams?.get('t') ?? ''; // TODO: choose a better name instead of t
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
@@ -26,9 +26,7 @@ export default function UnverifiedAccount() {
try {
setIsButtonDisabled(true);
- // TODO: decrypt email and send it
-
- await sendConfirmationEmail({ email: token ?? '' });
+ await sendConfirmationEmail({ email: encryptedEmail });
toast({
title: 'Success',
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 09ee0351f..510e2a6fd 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';
+import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
@@ -136,7 +137,9 @@ export const profileRouter = router({
try {
const { email } = input;
- return await sendConfirmationToken({ email });
+ const decryptedEmail = decryptSecondaryData(email);
+
+ return await sendConfirmationToken({ email: decryptedEmail ?? '' }); // TODO: fix this tomorrow
} catch (err) {
let message = 'We were unable to send a confirmation email. Please try again.';
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 671756e94..5aa9844ca 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -30,9 +30,9 @@ export const ZResetPasswordFormSchema = z.object({
password: z.string().min(6),
token: z.string().min(1),
});
-
+// TODO: revisit this
export const ZConfirmEmailMutationSchema = z.object({
- email: z.string().email().min(1),
+ email: z.string().min(1),
});
export type TRetrieveUserByIdQuerySchema = z.infer;
From e2fa01509dc602aedc9d9764a0f0f24e0e8208c5 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Thu, 25 Jan 2024 17:33:35 +0200
Subject: [PATCH 04/14] chore: avoid returning unnecessary info
---
packages/lib/server-only/user/send-confirmation-token.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/lib/server-only/user/send-confirmation-token.ts b/packages/lib/server-only/user/send-confirmation-token.ts
index 5206d202e..6c070125b 100644
--- a/packages/lib/server-only/user/send-confirmation-token.ts
+++ b/packages/lib/server-only/user/send-confirmation-token.ts
@@ -37,5 +37,12 @@ export const sendConfirmationToken = async ({ email }: { email: string }) => {
throw new Error(`Failed to create the verification token`);
}
- return sendConfirmationEmail({ userId: user.id });
+ // TODO: Revisit tomorrow
+ try {
+ await sendConfirmationEmail({ userId: user.id });
+
+ return { success: true };
+ } catch (err) {
+ throw new Error(`Failed to send the confirmation email`);
+ }
};
From b2cca9afb677da0505189aa5e0f68e57dc1250e5 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Fri, 26 Jan 2024 13:27:36 +0200
Subject: [PATCH 05/14] chore: refactor
---
.../app/(unauthenticated)/unverified-account/page.tsx | 4 ++--
apps/web/src/components/forms/signin.tsx | 2 +-
apps/web/src/components/forms/signup.tsx | 2 +-
.../lib/server-only/user/send-confirmation-token.ts | 1 -
packages/trpc/server/profile-router/router.ts | 10 +++++++---
packages/trpc/server/profile-router/schema.ts | 4 ++--
6 files changed, 13 insertions(+), 10 deletions(-)
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
index 5199249e0..dc98044ae 100644
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -18,7 +18,7 @@ export default function UnverifiedAccount() {
const searchParams = useSearchParams();
const { toast } = useToast();
- const encryptedEmail = searchParams?.get('t') ?? ''; // TODO: choose a better name instead of t
+ const encryptedEmail = searchParams?.get('token') ?? '';
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
@@ -26,7 +26,7 @@ export default function UnverifiedAccount() {
try {
setIsButtonDisabled(true);
- await sendConfirmationEmail({ email: encryptedEmail });
+ await sendConfirmationEmail({ encryptedEmail });
toast({
title: 'Success',
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
index 4e3701c84..0353333cf 100644
--- a/apps/web/src/components/forms/signin.tsx
+++ b/apps/web/src/components/forms/signin.tsx
@@ -134,7 +134,7 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
const encryptedEmail = await encryptSecondaryData({ data: email });
- router.push(`/unverified-account?t=${encryptedEmail}`);
+ router.push(`/unverified-account?token=${encryptedEmail}`);
return;
}
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index 190084226..bc7ee0ce5 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -70,7 +70,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
const encryptedEmail = await encryptSecondaryData({ data: email });
- router.push(`/unverified-account?t=${encryptedEmail}`);
+ router.push(`/unverified-account?token=${encryptedEmail}`);
toast({
title: 'Registration Successful',
diff --git a/packages/lib/server-only/user/send-confirmation-token.ts b/packages/lib/server-only/user/send-confirmation-token.ts
index 6c070125b..af4a97a48 100644
--- a/packages/lib/server-only/user/send-confirmation-token.ts
+++ b/packages/lib/server-only/user/send-confirmation-token.ts
@@ -37,7 +37,6 @@ export const sendConfirmationToken = async ({ email }: { email: string }) => {
throw new Error(`Failed to create the verification token`);
}
- // TODO: Revisit tomorrow
try {
await sendConfirmationEmail({ userId: user.id });
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 510e2a6fd..44d0f59bd 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -135,11 +135,15 @@ export const profileRouter = router({
.input(ZConfirmEmailMutationSchema)
.mutation(async ({ input }) => {
try {
- const { email } = input;
+ const { encryptedEmail } = input;
- const decryptedEmail = decryptSecondaryData(email);
+ const decryptedEmail = decryptSecondaryData(encryptedEmail);
- return await sendConfirmationToken({ email: decryptedEmail ?? '' }); // TODO: fix this tomorrow
+ if (!decryptedEmail) {
+ throw new Error('Email is required');
+ }
+
+ return await sendConfirmationToken({ email: decryptedEmail });
} catch (err) {
let message = 'We were unable to send a confirmation email. Please try again.';
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 5aa9844ca..897a4912d 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -30,9 +30,9 @@ export const ZResetPasswordFormSchema = z.object({
password: z.string().min(6),
token: z.string().min(1),
});
-// TODO: revisit this
+
export const ZConfirmEmailMutationSchema = z.object({
- email: z.string().min(1),
+ encryptedEmail: z.string().min(1),
});
export type TRetrieveUserByIdQuerySchema = z.infer;
From f514d55d27bc23866dd70b6b7c7005bce5891851 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Mon, 29 Jan 2024 09:41:02 +0200
Subject: [PATCH 06/14] chore: removed unused schema
---
packages/trpc/server/profile-router/schema.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 897a4912d..dfde69796 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -8,10 +8,6 @@ export const ZRetrieveUserByEmailMutationSchema = z.object({
email: z.string().email().min(1),
});
-export const ZRetrieveUserByVerificationTokenQuerySchema = z.object({
- token: z.string().min(1),
-});
-
export const ZUpdateProfileMutationSchema = z.object({
name: z.string().min(1),
signature: z.string(),
From 1676f5bf6cbcdc63b6cae10ee0be61e1a4be5d52 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Mon, 29 Jan 2024 09:43:38 +0200
Subject: [PATCH 07/14] chore: removed unused code
---
packages/trpc/server/profile-router/router.ts | 17 -----------------
packages/trpc/server/profile-router/schema.ts | 4 ----
2 files changed, 21 deletions(-)
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 44d0f59bd..1faa3c8e6 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -2,7 +2,6 @@ import { TRPCError } from '@trpc/server';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
-import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
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';
@@ -14,7 +13,6 @@ import {
ZConfirmEmailMutationSchema,
ZForgotPasswordFormSchema,
ZResetPasswordFormSchema,
- ZRetrieveUserByEmailMutationSchema,
ZRetrieveUserByIdQuerySchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
@@ -34,21 +32,6 @@ export const profileRouter = router({
}
}),
- getUserByEmail: procedure
- .input(ZRetrieveUserByEmailMutationSchema)
- .mutation(async ({ input }) => {
- try {
- const { email } = input;
-
- return await getUserByEmail({ email });
- } catch (err) {
- throw new TRPCError({
- code: 'BAD_REQUEST',
- message: 'We were unable to retrieve the specified account. Please try again.',
- });
- }
- }),
-
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index dfde69796..135d0d1e8 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -4,10 +4,6 @@ export const ZRetrieveUserByIdQuerySchema = z.object({
id: z.number().min(1),
});
-export const ZRetrieveUserByEmailMutationSchema = z.object({
- email: z.string().email().min(1),
-});
-
export const ZUpdateProfileMutationSchema = z.object({
name: z.string().min(1),
signature: z.string(),
From cc090adce0918def56279c44d76cecaa21bf5fe5 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 30 Jan 2024 12:54:48 +0200
Subject: [PATCH 08/14] chore: refactor
---
.../unverified-account/page.tsx | 53 ++-----------------
apps/web/src/components/forms/signin.tsx | 10 ++--
apps/web/src/components/forms/signup.tsx | 8 ++-
packages/lib/next-auth/auth-options.ts | 10 ++++
.../lib/server-only/user/get-user-by-email.ts | 3 ++
.../user/send-confirmation-token.ts | 4 ++
packages/trpc/server/profile-router/router.ts | 11 +---
packages/trpc/server/profile-router/schema.ts | 2 +-
8 files changed, 32 insertions(+), 69 deletions(-)
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
index dc98044ae..9b636f7cf 100644
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -1,51 +1,8 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
import { Mails } 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 { useToast } from '@documenso/ui/primitives/use-toast';
-
-const RESEND_CONFIRMATION_EMAIL_TIMEOUT = 20 * ONE_SECOND;
+import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-email';
export default function UnverifiedAccount() {
- const [isButtonDisabled, setIsButtonDisabled] = useState(false);
- const searchParams = useSearchParams();
- const { toast } = useToast();
-
- const encryptedEmail = searchParams?.get('token') ?? '';
-
- const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
-
- const onResendConfirmationEmail = async () => {
- try {
- setIsButtonDisabled(true);
-
- await sendConfirmationEmail({ encryptedEmail });
-
- toast({
- title: 'Success',
- description: 'Verification email sent successfully.',
- duration: 5000,
- });
-
- 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',
- });
- }
- };
-
return (
@@ -55,13 +12,11 @@ export default function UnverifiedAccount() {
Confirm email
- To gain full access to your account and unlock all its features, please confirm your email
- address by clicking on the link sent to your email address.
+ To gain access to your account, please confirm your email address by clicking on the
+ confirmation link from your inbox.
-
- Resend email
-
+
);
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
index 0353333cf..d0b5e1b60 100644
--- a/apps/web/src/components/forms/signin.tsx
+++ b/apps/web/src/components/forms/signin.tsx
@@ -11,7 +11,6 @@ import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
-import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
@@ -62,8 +61,6 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
useState(false);
const router = useRouter();
- const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
-
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
'totp' | 'backup'
>('totp');
@@ -132,9 +129,12 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
const errorMessage = ERROR_MESSAGES[result.error];
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
- const encryptedEmail = await encryptSecondaryData({ data: email });
+ router.push(`/unverified-account`);
- router.push(`/unverified-account?token=${encryptedEmail}`);
+ toast({
+ title: 'Unable to sign in',
+ description: errorMessage ?? 'An unknown error occurred',
+ });
return;
}
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index bc7ee0ce5..4520e00ca 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -62,19 +62,17 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
- const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
await signup({ name, email, password, signature });
- const encryptedEmail = await encryptSecondaryData({ data: email });
-
- router.push(`/unverified-account?token=${encryptedEmail}`);
+ router.push(`/unverified-account}`);
toast({
title: 'Registration Successful',
- description: 'You have successfully registered. Please sign in to continue.',
+ description:
+ 'You have successfully registered. Please verify your account by clicking on the link you received in the email.',
duration: 5000,
});
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index 37f1ed864..1dedfe12b 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -14,6 +14,7 @@ import { IdentityProvider } from '@documenso/prisma/client';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
+import { sendConfirmationToken } from '../server-only/user/send-confirmation-token';
import { ErrorCode } from './error-codes';
export const NEXT_AUTH_OPTIONS: AuthOptions = {
@@ -71,6 +72,15 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
if (!user.emailVerified) {
+ const totalUserVerificationTokens = user.VerificationToken.length;
+ const lastUserVerificationToken = user.VerificationToken[totalUserVerificationTokens - 1];
+ const expiredToken =
+ DateTime.fromJSDate(lastUserVerificationToken.expires) <= DateTime.now();
+
+ if (totalUserVerificationTokens < 1 || expiredToken) {
+ await sendConfirmationToken({ email });
+ }
+
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
}
diff --git a/packages/lib/server-only/user/get-user-by-email.ts b/packages/lib/server-only/user/get-user-by-email.ts
index 0a2ef8d16..8c61202a2 100644
--- a/packages/lib/server-only/user/get-user-by-email.ts
+++ b/packages/lib/server-only/user/get-user-by-email.ts
@@ -9,5 +9,8 @@ export const getUserByEmail = async ({ email }: GetUserByEmailOptions) => {
where: {
email: email.toLowerCase(),
},
+ include: {
+ VerificationToken: true,
+ },
});
};
diff --git a/packages/lib/server-only/user/send-confirmation-token.ts b/packages/lib/server-only/user/send-confirmation-token.ts
index af4a97a48..a399dd9fc 100644
--- a/packages/lib/server-only/user/send-confirmation-token.ts
+++ b/packages/lib/server-only/user/send-confirmation-token.ts
@@ -20,6 +20,10 @@ export const sendConfirmationToken = async ({ email }: { email: string }) => {
throw new Error('User not found');
}
+ if (user.emailVerified) {
+ throw new Error('Email verified');
+ }
+
const createdToken = await prisma.verificationToken.create({
data: {
identifier: IDENTIFIER,
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 1faa3c8e6..3d765372b 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,6 +1,5 @@
import { TRPCError } from '@trpc/server';
-import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
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';
@@ -118,15 +117,9 @@ export const profileRouter = router({
.input(ZConfirmEmailMutationSchema)
.mutation(async ({ input }) => {
try {
- const { encryptedEmail } = input;
+ const { email } = input;
- const decryptedEmail = decryptSecondaryData(encryptedEmail);
-
- if (!decryptedEmail) {
- throw new Error('Email is required');
- }
-
- return await sendConfirmationToken({ email: decryptedEmail });
+ return await sendConfirmationToken({ email });
} catch (err) {
let message = 'We were unable to send a confirmation email. Please try again.';
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 135d0d1e8..ef9ca2a14 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -24,7 +24,7 @@ export const ZResetPasswordFormSchema = z.object({
});
export const ZConfirmEmailMutationSchema = z.object({
- encryptedEmail: z.string().min(1),
+ email: z.string().email().min(1),
});
export type TRetrieveUserByIdQuerySchema = z.infer;
From 6053a4a40a55db36c815dbc2bdbf3c140f62860d Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 30 Jan 2024 12:56:32 +0200
Subject: [PATCH 09/14] chore: refactor
---
.../forms/send-confirmation-email.tsx | 93 +++++++++++++++++++
apps/web/src/components/forms/signup.tsx | 2 +-
2 files changed, 94 insertions(+), 1 deletion(-)
create mode 100644 apps/web/src/components/forms/send-confirmation-email.tsx
diff --git a/apps/web/src/components/forms/send-confirmation-email.tsx b/apps/web/src/components/forms/send-confirmation-email.tsx
new file mode 100644
index 000000000..9e669539e
--- /dev/null
+++ b/apps/web/src/components/forms/send-confirmation-email.tsx
@@ -0,0 +1,93 @@
+'use client';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+export const ZSendConfirmationEmailFormSchema = z.object({
+ email: z.string().email().min(1),
+});
+
+export type TSendConfirmationEmailFormSchema = z.infer;
+
+export type SendConfirmationEmailFormProps = {
+ className?: string;
+};
+
+export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFormProps) => {
+ const { toast } = useToast();
+
+ const form = useForm({
+ values: {
+ email: '',
+ },
+ resolver: zodResolver(ZSendConfirmationEmailFormSchema),
+ });
+
+ const isSubmitting = form.formState.isSubmitting;
+
+ const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
+
+ const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
+ try {
+ await sendConfirmationEmail({ email });
+
+ toast({
+ title: 'Confirmation email sent',
+ description:
+ 'A confirmation email has been sent, and it should arrive in your inbox shortly.',
+ duration: 5000,
+ });
+
+ form.reset();
+ } catch (err) {
+ toast({
+ title: 'An error occurred while sending your confirmation email',
+ description: 'Please try again and make sure you enter the correct email address.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index 4520e00ca..7bfe07968 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -67,7 +67,7 @@ export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) =
try {
await signup({ name, email, password, signature });
- router.push(`/unverified-account}`);
+ router.push(`/unverified-account`);
toast({
title: 'Registration Successful',
From 1852aa4b05717b474089382e9682c3a572fc1296 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 30 Jan 2024 12:57:56 +0200
Subject: [PATCH 10/14] chore: add info
---
.../web/src/app/(unauthenticated)/unverified-account/page.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
index 9b636f7cf..f4b8b90d7 100644
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
@@ -16,6 +16,10 @@ export default function UnverifiedAccount() {
confirmation link from your inbox.
+
+ If you don't find the confirmation link in your inbox, you can request a new one below.
+
+
From c432261dd8ff7756ad7bc9b1059944d7478fa1c1 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 30 Jan 2024 14:49:31 +0200
Subject: [PATCH 11/14] chore: disable button while form is submitting
---
apps/web/src/components/forms/send-confirmation-email.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/components/forms/send-confirmation-email.tsx b/apps/web/src/components/forms/send-confirmation-email.tsx
index 9e669539e..ee073d063 100644
--- a/apps/web/src/components/forms/send-confirmation-email.tsx
+++ b/apps/web/src/components/forms/send-confirmation-email.tsx
@@ -83,7 +83,7 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
)}
/>
-
+
Send confirmation email
From 149f416be76ef555f91a5e9bebacc5659ec826b0 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 13 Feb 2024 07:50:22 +0200
Subject: [PATCH 12/14] chore: refactor code
---
.../forms/send-confirmation-email.tsx | 50 ++++++++++---------
packages/lib/next-auth/auth-options.ts | 14 ++++--
.../user/get-last-verification-token.ts | 27 ++++++++++
.../lib/server-only/user/get-user-by-email.ts | 3 --
4 files changed, 64 insertions(+), 30 deletions(-)
create mode 100644 packages/lib/server-only/user/get-last-verification-token.ts
diff --git a/apps/web/src/components/forms/send-confirmation-email.tsx b/apps/web/src/components/forms/send-confirmation-email.tsx
index ee073d063..33247bf9f 100644
--- a/apps/web/src/components/forms/send-confirmation-email.tsx
+++ b/apps/web/src/components/forms/send-confirmation-email.tsx
@@ -13,6 +13,7 @@ import {
FormField,
FormItem,
FormLabel,
+ FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -63,31 +64,32 @@ export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFo
};
return (
-
-
-
+
+
+
);
};
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index d28506ca3..723b9cd7b 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -13,6 +13,7 @@ import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/cl
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
+import { getLastVerificationToken } from '../server-only/user/get-last-verification-token';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { sendConfirmationToken } from '../server-only/user/send-confirmation-token';
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
@@ -92,12 +93,19 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
if (!user.emailVerified) {
- const totalUserVerificationTokens = user.VerificationToken.length;
- const lastUserVerificationToken = user.VerificationToken[totalUserVerificationTokens - 1];
+ const [lastUserVerificationToken] = await getLastVerificationToken({ userId: user.id });
+
+ if (!lastUserVerificationToken) {
+ await sendConfirmationToken({ email });
+ throw new Error(ErrorCode.UNVERIFIED_EMAIL);
+ }
+
const expiredToken =
DateTime.fromJSDate(lastUserVerificationToken.expires) <= DateTime.now();
+ const lastSentToken = DateTime.fromJSDate(lastUserVerificationToken.createdAt);
+ const sentWithinLastHour = DateTime.now().minus({ hours: 1 }) <= lastSentToken;
- if (totalUserVerificationTokens < 1 || expiredToken) {
+ if (expiredToken || !sentWithinLastHour) {
await sendConfirmationToken({ email });
}
diff --git a/packages/lib/server-only/user/get-last-verification-token.ts b/packages/lib/server-only/user/get-last-verification-token.ts
new file mode 100644
index 000000000..279a1fcfd
--- /dev/null
+++ b/packages/lib/server-only/user/get-last-verification-token.ts
@@ -0,0 +1,27 @@
+import { prisma } from '@documenso/prisma';
+
+export interface GetLastVerificationTokenOptions {
+ userId: number;
+}
+
+export const getLastVerificationToken = async ({ userId }: GetLastVerificationTokenOptions) => {
+ const user = await prisma.user.findFirstOrThrow({
+ where: {
+ id: userId,
+ },
+ include: {
+ VerificationToken: {
+ select: {
+ expires: true,
+ createdAt: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ take: 1,
+ },
+ },
+ });
+
+ return user.VerificationToken;
+};
diff --git a/packages/lib/server-only/user/get-user-by-email.ts b/packages/lib/server-only/user/get-user-by-email.ts
index 8c61202a2..0a2ef8d16 100644
--- a/packages/lib/server-only/user/get-user-by-email.ts
+++ b/packages/lib/server-only/user/get-user-by-email.ts
@@ -9,8 +9,5 @@ export const getUserByEmail = async ({ email }: GetUserByEmailOptions) => {
where: {
email: email.toLowerCase(),
},
- include: {
- VerificationToken: true,
- },
});
};
From 4878cf388f35feb857fd1c701868b5f97bd46476 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Tue, 13 Feb 2024 07:53:36 +0200
Subject: [PATCH 13/14] chore: add the missing signIn function
---
apps/web/src/components/forms/signup.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index 087e71fbe..7082bcee3 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -3,6 +3,7 @@
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
+import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
From d052f0201325d590d5066efafeefbc8f2e6a1f69 Mon Sep 17 00:00:00 2001
From: Lucas Smith
Date: Tue, 13 Feb 2024 06:01:25 +0000
Subject: [PATCH 14/14] chore: refactor code
---
packages/lib/next-auth/auth-options.ts | 22 +++++++--------
.../user/get-last-verification-token.ts | 27 -------------------
...st-recent-verification-token-by-user-id.ts | 18 +++++++++++++
.../user/send-confirmation-token.ts | 20 +++++++++++++-
4 files changed, 46 insertions(+), 41 deletions(-)
delete mode 100644 packages/lib/server-only/user/get-last-verification-token.ts
create mode 100644 packages/lib/server-only/user/get-most-recent-verification-token-by-user-id.ts
diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts
index 723b9cd7b..b944b6e7b 100644
--- a/packages/lib/next-auth/auth-options.ts
+++ b/packages/lib/next-auth/auth-options.ts
@@ -13,7 +13,7 @@ import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/cl
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
-import { getLastVerificationToken } from '../server-only/user/get-last-verification-token';
+import { getMostRecentVerificationTokenByUserId } from '../server-only/user/get-most-recent-verification-token-by-user-id';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { sendConfirmationToken } from '../server-only/user/send-confirmation-token';
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
@@ -93,19 +93,15 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}
if (!user.emailVerified) {
- const [lastUserVerificationToken] = await getLastVerificationToken({ userId: user.id });
+ const mostRecentToken = await getMostRecentVerificationTokenByUserId({
+ userId: user.id,
+ });
- if (!lastUserVerificationToken) {
- await sendConfirmationToken({ email });
- throw new Error(ErrorCode.UNVERIFIED_EMAIL);
- }
-
- const expiredToken =
- DateTime.fromJSDate(lastUserVerificationToken.expires) <= DateTime.now();
- const lastSentToken = DateTime.fromJSDate(lastUserVerificationToken.createdAt);
- const sentWithinLastHour = DateTime.now().minus({ hours: 1 }) <= lastSentToken;
-
- if (expiredToken || !sentWithinLastHour) {
+ if (
+ !mostRecentToken ||
+ mostRecentToken.expires.valueOf() <= Date.now() ||
+ DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
+ ) {
await sendConfirmationToken({ email });
}
diff --git a/packages/lib/server-only/user/get-last-verification-token.ts b/packages/lib/server-only/user/get-last-verification-token.ts
deleted file mode 100644
index 279a1fcfd..000000000
--- a/packages/lib/server-only/user/get-last-verification-token.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { prisma } from '@documenso/prisma';
-
-export interface GetLastVerificationTokenOptions {
- userId: number;
-}
-
-export const getLastVerificationToken = async ({ userId }: GetLastVerificationTokenOptions) => {
- const user = await prisma.user.findFirstOrThrow({
- where: {
- id: userId,
- },
- include: {
- VerificationToken: {
- select: {
- expires: true,
- createdAt: true,
- },
- orderBy: {
- createdAt: 'desc',
- },
- take: 1,
- },
- },
- });
-
- return user.VerificationToken;
-};
diff --git a/packages/lib/server-only/user/get-most-recent-verification-token-by-user-id.ts b/packages/lib/server-only/user/get-most-recent-verification-token-by-user-id.ts
new file mode 100644
index 000000000..d9adc4498
--- /dev/null
+++ b/packages/lib/server-only/user/get-most-recent-verification-token-by-user-id.ts
@@ -0,0 +1,18 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetMostRecentVerificationTokenByUserIdOptions = {
+ userId: number;
+};
+
+export const getMostRecentVerificationTokenByUserId = async ({
+ userId,
+}: GetMostRecentVerificationTokenByUserIdOptions) => {
+ return await prisma.verificationToken.findFirst({
+ where: {
+ userId,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+};
diff --git a/packages/lib/server-only/user/send-confirmation-token.ts b/packages/lib/server-only/user/send-confirmation-token.ts
index a399dd9fc..ef7c4b104 100644
--- a/packages/lib/server-only/user/send-confirmation-token.ts
+++ b/packages/lib/server-only/user/send-confirmation-token.ts
@@ -1,13 +1,20 @@
import crypto from 'crypto';
+import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { ONE_HOUR } from '../../constants/time';
import { sendConfirmationEmail } from '../auth/send-confirmation-email';
+import { getMostRecentVerificationTokenByUserId } from './get-most-recent-verification-token-by-user-id';
const IDENTIFIER = 'confirmation-email';
-export const sendConfirmationToken = async ({ email }: { email: string }) => {
+type SendConfirmationTokenOptions = { email: string; force?: boolean };
+
+export const sendConfirmationToken = async ({
+ email,
+ force = false,
+}: SendConfirmationTokenOptions) => {
const token = crypto.randomBytes(20).toString('hex');
const user = await prisma.user.findFirst({
@@ -24,6 +31,17 @@ export const sendConfirmationToken = async ({ email }: { email: string }) => {
throw new Error('Email verified');
}
+ const mostRecentToken = await getMostRecentVerificationTokenByUserId({ userId: user.id });
+
+ // If we've sent a token in the last 5 minutes, don't send another one
+ if (
+ !force &&
+ mostRecentToken?.createdAt &&
+ DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
+ ) {
+ return;
+ }
+
const createdToken = await prisma.verificationToken.create({
data: {
identifier: IDENTIFIER,