Having explored how to implement an authentication system in Next.js 14 with NextAuth.js(Auth.js) in the first part of this blog, it is crucial to take the next step to ensure the validity of user information: email validation.
This process is not only an additional step in the security of our application, but an essential component to ensure that interactions between the user and the platform are legitimate and secure.
In this second part, we will focus on integrating email validation by sending emails, using Resend for sending emails, and React Email to create attractive and functional email templates.
Make sure your project already has the authentication system described in the first part of the blog implemented. This includes having Next.js 14 and NextAuth configured correctly.
Install the dependencies needed in the project. This time we’ll use pnpm
you can use the package manager of your choice.
pnpm add resend react-email @react-email/components
2. Create the following structure for the project:
...
├── emails/
│ └── verification-template.tsx
...
├── src/
│ ├── actions/
│ │ ├── email-actions.tsx
│ │ └── auth-actions.tsx
│ ├── app/
│ │ ...
│ │ ├── (primary)/
│ │ │ ├── auth/
│ │ │ │ └── verify-email/
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ │ ...
│ ├── components/
│ │ └── auth/
│ │ ├── signin-form.tsx
│ │ ├── signup-form.tsx
│ │ ...
│ ...
│ ├── utils.ts
│ ...
...
├── .env
...
React Email allows you to create email templates using JSX, which facilitates the creation of attractive and consistent emails with your brand.
Let’s create a basic email template as a React component. In this case, we will create the template that will be sent for the user to confirm their email.
emails/verification-template.tsx
:
// Import React and necessary components from @react-email/components
import * as React from 'react';
import { Body, Button, Container, Head, Hr, Html, Img, Preview, Section, Text } from '@react-email/components';
import { getBaseUrl } from '@/utils';
// Obtain the base URL using the imported function
const baseUrl = getBaseUrl();
// Define the properties expected by the VerificationTemplate component
interface VerificationTemplateProps {
username: string;
emailVerificationToken: string;
}
// Define the VerificationTemplate component that takes the defined properties
export const VerificationTemplate = ({ username, emailVerificationToken }: VerificationTemplateProps) => (
<Html>
<Head />
<Preview>Preview text that appears in the email client before opening the email.</Preview>
<Body style={main}>
<Container style={container}>
<Img
src='my-logo.png'
alt='My SaaS'
style={logo}
/>
<Text style={title}>Hi {username}!</Text>
<Text style={title}>Welcome to Starter Kit for building a SaaS</Text>
<Text style={paragraph}>Please verify your email, with the link below:</Text>
<Section style={btnContainer}>
{/* Button that takes the user to the verification link */}
<Button
style={button}
href={`${baseUrl}/auth/verify-email?token=${emailVerificationToken}`}
>
Click here to verify
</Button>
</Section>
<Hr style={hr} />
<Text style={footer}>Something in the footer.</Text>
</Container>
</Body>
</Html>
);
// Styles applied to different parts of the email for customization
const main = {
backgroundColor: '#020817',
color: '#ffffff',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif',
};
const container = {
margin: '0 auto',
padding: '20px 0 48px',
};
...
This component creates an HTML email template that includes styles and dynamic content.
Properties are defined to receive username
y emailVerificationToken
. These properties are used to customize the email for the user and generate the verification link.
To validate and test the template React Email provides a command to locally run a server that will expose the templates we have created inside the emails folder.
We create the script in the package.json
to run the server.
{
"scripts": {
"dev": "email dev"
}
}
2. We execute the script and this will run the server on localhost
; we will see a screen like the following with all the templates created.
In our case, we have only one template. As you can see below, we have a preview of the email that will be sent to the user.
API KEY
to .env
file.
...
# resend
RESEND_API_KEY="re_jYiFaXXXXXXXXXXXXXXXXXXX"
4. To create the functionality of sending emails, we could create endpoints inside the api/
folder and make http
requests, however, on this occasion we will do it by taking advantage of the server actions potential.
actions/email-actions.ts
:
'use server'
import React from 'react'
import { Resend } from 'resend'
// Creates an instance of Resend using the API KEY
const resend = new Resend(process.env.RESEND_API_KEY)
// Defines the data structure for an email.
interface Email {
to: string[] // An array of email addresses to which to send the email.
subject: string // The subject of the email.
react: React.ReactElement // The body of the email as a React element.
}
export const sendEmail = async (payload: Email) => {
const { error } = await resend.emails.send({
from: 'My SaaS <[email protected]>', // Defines the sender's address.
...payload, // Expands the contents of 'payload' to include 'to', 'subject', and 'react'.
})
if (error) {
console.error('Error sending email', error)
return null
}
console.log('Email sent successfully')
return true
}
Note: To test in free development, you have to use as the sender the email “[email protected]”, otherwise, you would have to add a custom domain.
5. Send the email when registering a new user.
actions/auth-actions.ts
:
...
import { sendEmail } from './email-actions'
import VerificationTemplate from '../../emails/verification-template'
// Import a utility function to generate a secure token.
import { generateSecureToken } from '@/utils'
export async function registerUser(user: Partial<User>) {
try {
// Creates a new user in the database with the provided data.
// Passwords are hashed using bcrypt for security.
const createdUser = await prisma.user.create({
data: {
...user,
password: await bcrypt.hash(user.password as string, 10),
} as User,
})
// Generates a secure token to be used for email verification.
const emailVerificationToken = generateSecureToken()
// Updates the newly created user with the email verification token.
await prisma.user.update({
where: {
id: createdUser.id,
},
data: {
emailVerificationToken,
},
})
// Sends a verification email to the new user using the sendEmail function.
await sendEmail({
to: ['your Resend registered email', createdUser.email],
subject: 'Verify your email address',
react: React.createElement(VerificationTemplate, { username: createdUser.username, emailVerificationToken }),
})
return createdUser
} catch (error) {
console.log(error)
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
// Returns a custom error message if the email already exists in the database.
return { error: 'Email already exists.' }
}
}
return { error: 'An unexpected error occurred.' }
}
}
Once the user is created and the verification token is generated, the function sends an email to the new user.
This email is constructed using the VerificationTemplate
React component, personalized with the user's name and verification token. This step is crucial for verifying the user's email address as valid and controlled by the user.
Once the email is sent to the user, this will have a link that will take him back to the site. To validate the email, for this, we need to create the page.
(primary)/auth/verify-email/page.tsx
:
/*
All imports
*/
// Defines the prop types for the VerifyEmailPage component.
interface VerifyEmailPageProps {
searchParams: { [key: string]: string | string[] | undefined }
}
export default async function VerifyEmailPage({ searchParams }: VerifyEmailPageProps) {
let message = 'Verifying email...'
let verified = false
if (searchParams.token) { // Checks if a verification token is provided in the URL.
// Attempts to find a user in the database with the provided email verification token.
const user = await prisma.user.findUnique({
where: {
emailVerificationToken: searchParams.token as string,
},
})
// Conditionally updates the message and verified status based on the user lookup.
if (!user) {
message = 'User not found. Check your email for the verification link.'
} else {
// If the user is found, updates the user record to mark the email as verified.
await prisma.user.update({
where: {
emailVerificationToken: searchParams.token as string,
},
data: {
emailVerified: true,
emailVerificationToken: null, // Clears the verification token.
},
})
message = `Email verified! ${user.email}`
verified = true // Sets the verified status to true.
}
} else {
// Updates the message if no verification token is found.
message = 'No email verification token found. Check your email.'
}
return (
<div className='grid place-content-center py-40'>
<Card className='max-w-sm text-center'>
<CardHeader>
<CardTitle>Email Verification</CardTitle>
</CardHeader>
<CardContent>
<div className='w-full grid place-content-center py-4'>
{verified ? <EmailCheckIcon size={56} /> : <EmailWarningIcon size={56} />}
</div>
<p className='text-lg text-muted-foreground' style={{ textWrap: 'balance' }}>
{message}
</p>
</CardContent>
<CardFooter>
{verified && (
// Displays a sign-in link if the email is successfully verified.
<Link href={'/auth/signin'} className='bg-primary text-white text-sm font-medium hover:bg-primary/90 h-10 px-4 py-2 rounded-lg w-full text-center'>
Sign in
</Link>
)}
</CardFooter>
</Card>
</div>
)
}
After successfully validating the user’s email, we will see the following message.
Now, we will implement a last validation for when the user wants to log in and has not yet verified his email.
components/auth/sigin-form.tsx
:
...
async function onSubmit(values: InputType) {
try {
setIsLoading(true)
const response = await signIn('credentials', {
redirect: false,
email: values.email,
password: values.password,
})
if (!response?.ok) {
// if the email is not verified we will show a message to the user.
if (response?.error === 'EmailNotVerified') {
toast({
title: 'Please, verify your email first.',
variant: 'warning',
})
return
}
toast({
title: 'Something went wrong!',
description: response?.error,
variant: 'destructive',
})
return
}
toast({
title: 'Welcome back! ',
description: 'Redirecting you to your dashboard!',
})
router.push(callbackUrl ? callbackUrl : '/')
} catch (error) {
console.log({ error })
toast({
title: 'Something went wrong!',
description: "We couldn't create your account. Please try again later!",
variant: 'destructive',
})
} finally {
setIsLoading(false)
}
}
...
That’s it! 🎉
The user will be able to validate his email and finish his registration in our application.
🧑💻
We already know how to create and send emails using React Email and Resend. This process allows you to leverage your knowledge of React to design emails efficiently while maintaining a familiar and productive workflow.
You can experiment with different components and properties to create emails that perfectly fit the needs of your projects.
Want to connect with the Author?
Love connecting with friends all around the world on 𝕏.
Also published here