4 min read

How to validate a user's email during registration

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:

  1. The user signs up with their email and a password. The user is inactive to start.
  2. Any inactive user cannot log in.
  3. A long, secure token is generated and emailed to the user in the form of a link. It links back to your app.
  4. When the user gets the email and clicks the link, the user is looked up in your app and marked as active.
  5. 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 ActivateTokens. 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.

⚠️
I don't handle errors in this sample code. You'll want to redirect instead of throwing a 500.

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')
}

Full Code

GitHub - ethanmick/email-activate-user
Contribute to ethanmick/email-activate-user development by creating an account on GitHub.