From 91c2bb6f537a0d3d9e8ab00f1ff65dfe13bf490b Mon Sep 17 00:00:00 2001 From: Hassan Ben Jobrane Date: Wed, 3 Apr 2024 12:52:47 +0100 Subject: [PATCH] feat: dashboard: restrict signup/signin to verified emails only (#2610) resolves https://github.com/nhost/nhost/issues/2585 --- .changeset/poor-adults-unite.md | 5 ++ dashboard/src/pages/email/verify.tsx | 94 ++++++++++++++++++++++++++++ dashboard/src/pages/signin/email.tsx | 14 ++++- dashboard/src/pages/signup.tsx | 12 +++- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 .changeset/poor-adults-unite.md create mode 100644 dashboard/src/pages/email/verify.tsx diff --git a/.changeset/poor-adults-unite.md b/.changeset/poor-adults-unite.md new file mode 100644 index 0000000000..486f0b3a00 --- /dev/null +++ b/.changeset/poor-adults-unite.md @@ -0,0 +1,5 @@ +--- +'@nhost/dashboard': minor +--- + +feat: refactor sign-in and sign-up pages to enforce email verification diff --git a/dashboard/src/pages/email/verify.tsx b/dashboard/src/pages/email/verify.tsx new file mode 100644 index 0000000000..dff4678b33 --- /dev/null +++ b/dashboard/src/pages/email/verify.tsx @@ -0,0 +1,94 @@ +import { NavLink } from '@/components/common/NavLink'; +import { UnauthenticatedLayout } from '@/components/layout/UnauthenticatedLayout'; +import { Box } from '@/components/ui/v2/Box'; +import { Button } from '@/components/ui/v2/Button'; +import { Text } from '@/components/ui/v2/Text'; +import { getToastStyleProps } from '@/utils/constants/settings'; +import { useNhostClient } from '@nhost/nextjs'; +import { useRouter } from 'next/router'; +import { useEffect, useState, type ReactElement } from 'react'; +import { toast } from 'react-hot-toast'; + +export default function VerifyEmailPage() { + const router = useRouter(); + const nhost = useNhostClient(); + + const { + query: { email }, + } = router; + + const [resendVerificationEmailLoading, setResendVerificationEmailLoading] = + useState(false); + + useEffect(() => { + if (!email) { + router.push('/signin'); + } + }, [email, router]); + + const resendVerificationEmail = async () => { + setResendVerificationEmailLoading(true); + + try { + await nhost.auth.sendVerificationEmail({ email: email as string }); + + toast.success( + `An new email has been sent to ${email}. Please follow the link to verify your email address and to + complete your registration.`, + getToastStyleProps(), + ); + } catch { + toast.error( + 'An error occurred while sending the verification email. Please try again.', + getToastStyleProps(), + ); + } finally { + setResendVerificationEmailLoading(false); + } + }; + + return ( + <> + + Verify your email + + + +
+ + Please check your inbox for the verification email. Follow the link + to verify your email address and complete your registration. + +
+ + +
+ + Sign In + +
+
+ + ); +} + +VerifyEmailPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; diff --git a/dashboard/src/pages/signin/email.tsx b/dashboard/src/pages/signin/email.tsx index 6a3f1d4e7a..4811ffff18 100644 --- a/dashboard/src/pages/signin/email.tsx +++ b/dashboard/src/pages/signin/email.tsx @@ -9,8 +9,8 @@ import { getToastStyleProps } from '@/utils/constants/settings'; import { yupResolver } from '@hookform/resolvers/yup'; import { styled } from '@mui/material'; import { useSignInEmailPassword } from '@nhost/nextjs'; -import type { ReactElement } from 'react'; -import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useEffect, type ReactElement } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import * as Yup from 'yup'; @@ -31,6 +31,7 @@ const StyledInput = styled(Input)({ export default function EmailSignUpPage() { const { signInEmailPassword, error } = useSignInEmailPassword(); + const router = useRouter(); const form = useForm({ reValidateMode: 'onSubmit', @@ -56,7 +57,14 @@ export default function EmailSignUpPage() { async function handleSubmit({ email, password }: EmailSignUpFormValues) { try { - await signInEmailPassword(email, password); + const { needsEmailVerification } = await signInEmailPassword( + email, + password, + ); + + if (needsEmailVerification) { + router.push(`/email/verify?email=${email}`); + } } catch { toast.error( 'An error occurred while signing in. Please try again.', diff --git a/dashboard/src/pages/signup.tsx b/dashboard/src/pages/signup.tsx index 743568d4e6..b2c807efe3 100644 --- a/dashboard/src/pages/signup.tsx +++ b/dashboard/src/pages/signup.tsx @@ -12,6 +12,7 @@ import { nhost } from '@/utils/nhost'; import { yupResolver } from '@hookform/resolvers/yup'; import { styled } from '@mui/material'; import { useSignUpEmailPassword } from '@nhost/nextjs'; +import { useRouter } from 'next/router'; import type { ReactElement } from 'react'; import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; @@ -36,6 +37,7 @@ const StyledInput = styled(Input)({ export default function SignUpPage() { const { signUpEmailPassword, error } = useSignUpEmailPassword(); const [loading, setLoading] = useState(false); + const router = useRouter(); const form = useForm({ reValidateMode: 'onSubmit', @@ -65,7 +67,15 @@ export default function SignUpPage() { displayName, }: SignUpFormValues) { try { - await signUpEmailPassword(email, password, { displayName }); + const { needsEmailVerification } = await signUpEmailPassword( + email, + password, + { displayName }, + ); + + if (needsEmailVerification) { + router.push(`/email/verify?email=${email}`); + } } catch { toast.error( 'An error occurred while signing up. Please try again.',