How to validate a user's email during registration
Adding email verification is an important step in building a SaaS product. It will ensure your users have a valid email for password resets and will cut down on spam. It's one of the first things you can do when your product is growing quickly to ensure the users are valid.
When a user registers, you'll need to capture their email address. Usually, this is used as the primary key for looking up the user later and logging in.
The flow for this activation is as follows:
- The user signs up with their email and a password. The user is
inactive
to start. - Any inactive user cannot log in.
- A long, secure token is generated and emailed to the user in the form of a link. It links back to your app.
- When the user gets the email and clicks the link, the user is looked up in your app and marked as active.
- The user can now log in.
Let's dive into the details!
Data Model
To handle activation, a new key should be added to the user to know if the user is active or not.
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String?
active Boolean @default(false) // New
activateTokens ActivateToken[]
}
You'll also need a way to track the ActivateToken
s. I think it's a good idea to make it a separate table so you can see the history of ActivateTokens. This way, you have a built-in audit trail of when a user tried to activate.
model ActivateToken {
id Int @id @default(autoincrement())
token String @unique
createdAt DateTime @default(now())
activatedAt DateTime?
user User @relation(fields: [userId], references: [id])
userId Int
}
Register
The registration process is the same if you weren't validating an email. Upon signup, create a User
object in your database for the user. This time, however, ensure the user is not active and then create a new ActivateToken
as well.
const token = await prisma.activateToken.create({
data: {
userId: user.id,
token: `${randomUUID()}${randomUUID()}`.replace(/-/g, ''),
},
})
Here, I create a new token for the new user. The token is just two secure UUIDs merged together, with all the dashes removed.
You'll want to add a check in your login function to ensure a user that is not active cannot log in.
Checking Login
Here is a snippet of the authorize
code that checks if a user can log in. Besides checking if the user has valid credentials, you also need to ensure they are active.
I add it as a separate check so the user can get a different error message.
async authorize(credentials) {
if (!credentials?.email || !credentials.password) {
return null
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email,
},
})
// User was not found at all, incorrect email
if (!user) {
return null
}
// User is not active, tell them to activate
if (!user.active) {
throw new Error('User is not active')
}
// ... rest of the function
},
})
Then, you need to send an email with that token.
Email the User
You'll need a transactional email sender, like Mailgun, for this. Once you have an account and your API key, it's easy to send an email:
const mailgun = new Mailgun(formData)
const client = mailgun.client({ username: 'api', key: API_KEY })
const messageData = {
from: `Example Email <hello@your-domain.com>`,
to: user.email,
subject: 'Please Activate Your Account',
text: `Hello ${user.name}, please activate your account by clicking this link: http://localhost:3000/activate/${token.token}`,
}
await client.messages.create(DOMAIN, messageData)
Here, the link points to localhost
, but you should make it an environment variable so it can be adjusted per environment. The link points back to your app and will load a specific route that will handle the activation. In this case, the /activate/
route with the token
as the page parameter.
Activate the Account
When the user clicks a link we're going to handle that request with an API route. This way we don't need to load a page first and then make an API request. After the activation is finished, we redirect the user to the login page. If something went wrong, we should redirect them to an error page with the error.
Here's how you can activate the user:
import { prisma } from '@/lib/prisma'
import { redirect } from 'next/navigation'
import { NextRequest } from 'next/server'
export async function GET(
_request: NextRequest,
{
params,
}: {
params: { token: string }
}
) {
const { token } = params
const user = await prisma.user.findFirst({
where: {
activateTokens: {
some: {
AND: [
{
activatedAt: null,
},
{
createdAt: {
gt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago
},
},
{
token
},
],
},
},
},
})
if (!user) {
throw new Error('Token is invalid or expired')
}
await prisma.user.update({
where: {
id: user.id,
},
data: {
active: true,
},
})
await prisma.activateToken.update({
where: {
token,
},
data: {
activatedAt: new Date(),
},
})
redirect('/api/auth/signin')
}