How I build a full-fledged app with Next.js and MongoDB Part 3: Email Verification, Password Reset/Change
Published: 2020-01-19 01:31:40

I have come back after a long hiatus in building [nextjs-mongodb-app](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app"). This time I am adding the following features:

- Email Verification
- Password Reset
- Password Change

Before this, I have made several modifications to the codebase, which you can look at in the following PRs:

- Rewrite with next-connect ([#22](https://github.com/hoangvvo/nextjs-mongodb-app/pull/22 "Rewrite with next-connect PR"))
- Replace axioswal with fetch (a custom fetch "fetchSwal") ([#41](https://github.com/hoangvvo/nextjs-mongodb-app/pull/41))
- The API response schema slightly changes. (`{ status: 'ok' }` becomes `{ ok: true }`)

Again, Below are the Github repository and a demo for this project to follow along.

[Github repo](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app")

[Demo](https://nextjs-mongodb-app.hoangvvo.now.sh/)

## What we are making

We are working on several features, which all involve email transaction.

- **Email Verification** allows you to verify the emails users used to sign up by sending them a verification link.

- **Password Reset** allows the users to reset their passwords from a reset link sent to their emails.

- **Password Change** allows the users to change their passwords simply by inputting their old and new passwords.

## Building Password Change

This is the easiest feature to implement. All we have to do is match the user's old password against the database and save their new one.

### Password Change Section

For simplicity, I added the section right below Profile Settings page.

![Password Reset in Profile Settings Page](//images.ctfassets.net/rgbpfpd0hrww/3p8JfnLd7AHC3R6ZIyok5F/f5093251d8378d17134912a0e988bced/Password_Reset_in_Profile_Settings_Page.png)

```jsx
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const handleSubmitPasswordChange = event => {
  event.preventDefault();
  fetchSwal
    .put("/api/user/password", { oldPassword, newPassword })
    .then(data => {
      if (data.ok) {
        setNewPassword("");
        setOldPassword("");
      }
    });
};
/* ... */
return (
  <form onSubmit={handleSubmitPasswordChange}>
    <label htmlFor="oldpassword">
      Old Password
      <input
        type="password"
        id="oldpassword"
        value={oldPassword}
        onChange={e => setOldPassword(e.target.value)}
        required
      />
    </label>
    <label htmlFor="newpassword">
      New Password
      <input
        type="password"
        id="newpassword"
        value={newPassword}
        onChange={e => setNewPassword(e.target.value)}
        required
      />
    </label>
    <button type="submit">Change Password</button>
  </form>
);
```

When the user submits, we make a PUT call to `/api/user/password` with the old and new password. If the request is successful, we clear the new and old password fields.

We now go ahead and create our API at `/api/user/password`.

### Password Change API

Create `/pages/api/user/password/index.js`.

```javascript
import nextConnect from 'next-connect';
import bcrypt from 'bcryptjs';
import middleware from '../../../../middlewares/middleware';

const handler = nextConnect();
handler.use(middleware);

handler.put(async (req, res) => {
  try {
    if (!req.user) throw new Error('You need to be logged in.');
    const { oldPassword, newPassword } = req.body;
    if (!(await bcrypt.compare(oldPassword, req.user.password))) {
      throw new Error('The password you has entered is incorrect.');
    }
    const password = await bcrypt.hash(newPassword, 10);
    await req.db
      .collection('users')
      .updateOne({ _id: req.user._id }, { $set: { password } });
    res.json({ message: 'Your password has been updated.' });
  } catch (error) {
    res.json({
      ok: false,
      message: error.toString(),
    });
  }
});

export default handler;
```

We are reusing our middleware instance (with database, session, authentication, etc.)

We first check if the user is logged in (`!req.user`) and reject the request if they are not. `req.user` also contains all of our user data (including the hashed password at `req.user.password`)

We then go ahead and retrieve `oldPassword` and `newPassword` from the request body. The `oldPassword` is compared against the *hashed* current password (`bcrypt.compare(oldPassword, req.user.password)`). If it matches, we reject the request. If it does we hash the new password (`bcrypt.hash(newPassword, 10)`) and save it in our database.

```javascript
await req.db
  .collection('users')
  .updateOne({ _id: req.user._id }, { $set: { password } });
```

Our `try/catch` makes it convenient to simply throw an error at any point to inform the user if the request is a failure.

And we are ready to roll~

![Password Change demo](//images.ctfassets.net/rgbpfpd0hrww/6Ngy9cDvBzq1NB7hVaKN9f/f795941addfcc40defa5775ac32b2d87/Password_Change_demo.gif)

## Password Reset

Now that the user can change their current password to a new one. Yet that is only when they know their current password. We are now implementing password reset.

### Forget Password API

We will have two routes for this API.

- `/pages/api/user/password/reset/index.js`: Handle requests to create a password reset token
- `/pages/api/user/password/reset/[token].js`: Handle the token to reset password.

Create `/pages/api/user/password/reset/index.js`.

```javascript
import sgMail from '@sendgrid/mail';
import crypto from 'crypto';
import nextConnect from 'next-connect';
import database from '../../../../../middlewares/database';

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const handler = nextConnect();

handler.use(database);

handler.post(async (req, res) => {
  try {
    const user = await req.db
      .collection('users')
      .findOne({ email: req.body.email });
    if (!user)
      throw new Error('This email is not associated with any account.');
    const token = crypto.randomBytes(32).toString('hex');
    await req.db.collection('tokens').insertOne({
      token,
      userId: user._id,
      type: 'passwordReset',
      expireAt: new Date(Date.now() + 1000 * 60 * 20)
    });
    const msg = {
      to: user.email,
      from: process.env.EMAIL_FROM,
      templateId: process.env.SENDGRID_TEMPLATEID_PASSWORDRESET,
      dynamic_template_data: {
        subject: '[nextjs-mongodb-app] Reset your password.',
        name: user.name,
        url: `${process.env.WEB_URI}/forgetpassword/${token}`
      }
    };
    await sgMail.send(msg);
    res.json({ message: 'An email has been sent to your inbox.' });
  } catch (error) {
    res.json({
      ok: false,
      message: error.toString()
    });
  }
});

export default handler;
```

We only need our database middleware in this API because we only need to communicate to the database.

We first verify if the email user input exists in the database `req.db.collection('users').findOne({ email: req.body.email })`. If the user exists, we create a `passwordReset` token with the `userId` attached. The token is generated by Node module `crypto` (although you can replace it with anything). 

set to be expire after 20 minutes for security reason: `expireAt: new Date(Date.now() + 1000 * 60 * 20)`. In `/middlewares/database.js`, we set the `tokens` collection to expire the token based on `expireAt` field:

```javascript
db
  .collection('tokens')
  .createIndex('expireAt', { expireAfterSeconds: 0 });
```

The token is then inserted into our `tokens` MongoDB collection.

We then send an email to the user with a password reset link (`website_url/forgetpassword/{token}`).

### Forget Password page

![Password Reset Page](//images.ctfassets.net/rgbpfpd0hrww/5MtfDKw9pRTdBl8vV4pH4X/794405aae1e8685bf25d267cd567719b/Password_Reset_Page.png "Password Reset Page")

We now create a forget password page at `/pages/forgetpassword/index.jsx`

```jsx
const ForgetPasswordPage = () => {
  const [email, setEmail] = useState();

  function handleSubmit(event) {
    event.preventDefault();
    fetchSwal
      .post('/api/user/password/reset', { email })
      .then(resp => resp.ok !== false && redirectTo('/'));
  }

  return (
    <Layout>
      <Head>
        <title>Forget password</title>
      </Head>
      <h2>Forget password</h2>
      <form onSubmit={handleSubmit}>
        <p>Do not worry. Simply enter your email address below.</p>
        <label htmlFor="email">
          <input
            id="email"
            type="email"
            placeholder="Email"
            value={email}
            onChange={e => setEmail(e.target.value)}
          />
        </label>
        <button type="submit">Submit</button>
      </form>
    </Layout>
  );
};
```

We simply ask the user for their email, which we then send to our just-created API above at '/api/user/password/reset'.

### Reset Password API

We need an API to resolve the reset token.

Create `/pages/api/user/password/reset/[token].js`:

```javascript
import nextConnect from 'next-connect';
import bcrypt from 'bcryptjs';
import database from '../../../../../middlewares/database';

const handler = nextConnect();

handler.use(database);

handler.post(async (req, res) => {
  // check valid token
  const tokenDoc = await req.db.collection('tokens').findOne({
    token: req.query.token,
    type: 'passwordReset'
  });
  res.end(tokenDoc ? 'true' : 'false');
});

handler.put(async (req, res) => {
  // password reset
  try {
    if (!req.body.password) throw new Error('No password provided.');
    const { value: tokenDoc } = await req.db
      .collection('tokens')
      .findOneAndDelete({ _id: req.query.token, type: 'passwordReset' });
    if (!tokenDoc) throw new Error('This link may have been expired.');
    const password = await bcrypt.hash(req.body.password, 10);
    await req.db
      .collection('users')
      .updateOne({ _id: tokenDoc.userId }, { $set: { password } });
    res.json({ message: 'Your password has been updated.' });
  } catch (error) {
    res.json({
      ok: false,
      message: error.toString()
    });
  }
});

export default handler;
```

There are two handlers: a `POST` and `PUT` one. 

The `POST` one will only check if the token is valid by querying `tokens` document, which then returns either `true` or `false`. This can be used to immediately show to the user that clicks the link if the token (or link) is valid (and optionally allowing them to request a new one). Later, we will see this check in `getInitialProps`.

The `PUT` one will do the same check for the token as in `POST`, but we also delete the token (`findOneAndDelete`) if it is found. If the token is found (and deleted), we hash the new password `bcrypt.hash(req.body.password, 10)` and save it to the user whose id is attached to the token (`tokenDoc.userId`):

```javascript
req.db
  .collection('users')
  .updateOne({ _id: tokenDoc.userId }, { $set: { password } });
```

### Reset Password page

This page represents the link that is sent to the user's email (`website_url/forgetpassword/{token}`). Create `/pages/forgetpassword/[token].jsx`:

```jsx
const ResetPasswordTokenPage = ({ valid, token }) => {
  const [password, setPassword] = useState('');
  function handleSubmit(event) {
    event.preventDefault();
    fetchSwal
      .post(`/api/user/password/reset/${token}`, { password })
      .then(resp => resp.ok !== false && redirectTo('/'));
  }

  return (
    <Layout>
      <Head>
        <title>Forget password</title>
      </Head>
      <h2>Forget password</h2>
      {valid ? (
        <>
          <p>Enter your new password.</p>
          <form onSubmit={handleSubmit}>
            <div>
              <input
                type="password"
                placeholder="New password"
                value={password}
                onChange={e => setPassword(e.target.value)}
              />
            </div>
            <button type="submit">Set new password</button>
          </form>
        </>
      ) : (
        <p>This link may have been expired</p>
      )}
    </Layout>
  );
};

ResetPasswordTokenPage.getInitialProps = async ctx => {
  const { token } = ctx.query;
  const valid = await fetch(
    `${process.env.WEB_URI}/api/user/password/reset/${token}`,
    { method: 'POST' }
  )
    .then(res => res.text())
    .then(bol => bol === 'true');
  return { token, valid };
};

export default ResetPasswordTokenPage;
```

As we can see in `getInitialProps`, we first query the `POST` handler to check if the token is valid and return it as a `pageProp` along with the token.

Based on the `valid` pageProp, we have a conditional render to show the password reset form if the token is valid or the text "This link may have been expired" otherwise:

```jsx
return (
  valid ? (
    <>
      <p>Enter your new password.</p>
      <form onSubmit={handleSubmit}>
        <div>
          <input
            type="password"
            placeholder="New password"
            value={password}
            onChange={e => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">Set new password</button>
      </form>
    </>
  ) : (
    <p>This link may have been expired</p>
  );
)
```

After the user input their new password, the new password is sent along with the token to our `PUT` handler.

![Password Reset Page Token](//images.ctfassets.net/rgbpfpd0hrww/2RxYhUXiNzGKoLI0DBgDBW/318c5b79482eec976130b79e6dcac841/Password_Reset_Page_Token.png "Password Reset Token Page")

That is how we do password reset.

## Building Email Verification

Let's finish up with email verification. This feature is similar to password reset as we similarly are sending a token to the user.

### Email Verification API

Get started by creating `/pages/api/user/email/verify/index.js`. This will handle users' requests to receive a confirmation email.

```javascript
const handler = nextConnect();

handler.use(middleware);

handler.post(async (req, res) => {
  try {
    if (!req.user) throw new Error('You need to be logged in.');
    const token = crypto.randomBytes(32).toString('hex');
    await req.db.collection('tokens').insertOne({
      token,
      userId: req.user._id,
      type: 'emailVerify',
      expireAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
    });
    const msg = {
      to: req.user.email,
      from: process.env.EMAIL_FROM,
      templateId: process.env.SENDGRID_TEMPLATEID_EMAILVERIFY,
      dynamic_template_data: {
        subject: '[nextjs-mongodb-app] Please verify your email address.',
        name: req.user.name,
        url: `${process.env.WEB_URI}/api/user/email/verify/${token}`,
      },
    };
    await sgMail.send(msg);
    res.json({ message: 'An email has been sent to your inbox.' });
  } catch (error) {
    res.json({
      ok: false,
      message: error.toString(),
    });
  }
});

export default handler;
```

In this handler, we are checking if the user is logged in and create a token that associates the user ID `req.user._id`. This token will expire in 24 hours.

We then send an email with the email confirmation link (`website_url/api/user/email/verify/{token}`).

Okay, I actually got lazy and did not create a dedicated page like we did above with Password Reset. This is the front of our API.

Looking at it, you will realize we need at `GET` handler at `/api/user/email/verify/{token}`.

Create `/pages/api/user/email/verify/[token].js`:

```javascript
handler.get(async (req, res) => {
  try {
    const { token } = req.query;
    const { value: tokenDoc } = await req.db
      .collection('tokens')
      .findOneAndDelete({ token, type: 'emailVerify' });

    if (!tokenDoc) {
      res.status(401).json({
        ok: false,
        message: 'This link may have been expired.',
      });
      return;
    }

    await req.db
      .collection('users')
      .updateOne({ _id: tokenDoc.userId }, { $set: { emailVerified: true } });

    res.json({
      ok: true,
      message: 'Success! Thank you for verifying your email address. You may close this page.',
    });
  } catch (error) {
    res.json({
      ok: false,
      message: error.toString(),
    });
  }
});
```

Similar to Password Reset, we check if the token exists and remove it.

```javascript
req.db
  .collection('tokens')
  .findOneAndDelete({ token, type: 'emailVerify' });
```

If it existed (and removed), we set the `emailVerified` field in the user's document to `true`.

### Expose the email verification status in the front-end

In `/pages/api/session.js`, I include `emailVerified` field:

```javascript
const { name, email, bio, profilePicture, emailVerified } = req.user;
return res.json({
  data: {
    isLoggedIn: true,
    user: {
      name,
      email,
      bio,
      profilePicture,
      emailVerified
    }
  }
});
```

The value of `emailVerified` will be used in our profile page at `pages/profile/index.js`.

```jsx
const {
  state: {
    isLoggedIn,
    user: { name, email, bio, profilePicture, emailVerified }
  }
} = useContext(UserContext);

function sendVerificationEmail() {
  fetchSwal.post('/api/user/email/verify');
}
/* ... */
return (
  <>
    {/* ... */}
    Email
    <p>
      {email}
      {!emailVerified ? (
        <>
          {' '}
          unverified {/* eslint-disable-next-line */}
          <a role="button" onClick={sendVerificationEmail}>
            Send verification email
          </a>
        </>
      ) : null}
    </p>
    {/* ... */}
  </>
);
```

If `emailVerified` is not `true`, we display a `unverified` text with a button that allows user to send a verification email. The button send a `POST` request to `/api/user/email/verify`, which we went through above.

## Conclusion

Tadah! We have managed to have password change/reset and email verification feature in our app. These features can be seen in real-life app.

Check out the repository [here](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app"). The pull request for this particular feature is [here](https://github.com/hoangvvo/nextjs-mongodb-app/pull/3/).

I have spent hours writing this articles and even dozen more working on this project. If you find it helpful, I would love to see it getting stared. Until then, good luck on your Next.js project!