DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 966,155 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Pablo DΓ­az
Pablo DΓ­az

Posted on

Next.js Full Example of Next-Iron-Session with Mongoose

Full Example

I couldn't find a complete example of Next.js authentication with next-iron-session so I used all the little pieces I found online and created one to share.

The Code

Before talking about authentication we need to setup the model of the User. To do this we will be using mongoose for flexibility and ease of usage. You can ignore the usage of httpStatus and APIError. Here is the user.model.js inside a models folder at root level.

import mongoose from 'mongoose';
import httpStatus from 'http-status';
import APIError from '@/lib/APIError';

/**
 * User Schema
 */
const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: false,
    lowercase: false,
    trim: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
  },
  password: {
    type: String,
    required: true,
  },
  updatedAt: {
    type: Date,
    default: Date.now,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

/**
 * Add your
 * - pre-save hooks
 * - validations
 * - virtuals
 */

/**
 * Methods
 */
UserSchema.method({});

/**
 * Statics
 */
UserSchema.statics = {
  /**
   * Get user
   * @param {string} id - The email of user.
   * @returns {Promise<User, APIError>}
   */
  get(email) {
    return this.findOne({ email });
      .select(
        'name email'
      )
      .exec()
      .then((user) => {
        if (user) {
          return user;
        }
        const err = new APIError(
          'User id does not exist',
          httpStatus.NOT_FOUND
        );
        return Promise.reject(err);
      });
  },
  /**
   * List users in descending order of 'createdAt' timestamp.
   * @param {number} skip - Number of users to be skipped.
   * @param {number} limipt - Limit number of users to be returned.
   * @returns {Promise<User[]>}
   */
  list({ skip = 0, limit = 50 } = {}) {
    return this.find()
      .select(
        'name email createdAt updatedAt '
      )
      .sort({ createdAt: -1 })
      .skip(+skip)
      .limit(+limit)
      .exec();
  },
};

/**
 * @typedef User
 */
export default mongoose.models.User || mongoose.model('User', UserSchema);

Enter fullscreen mode Exit fullscreen mode

One important detail to see here is the last line, Next.js will try to load the same file creating a duplicate instance and will give an error without the statement.

export default mongoose.models.User || mongoose.model('User', UserSchema);
Enter fullscreen mode Exit fullscreen mode

Config the Database

Create a file with the mongoose connection. Also see the part were the connection is saved in local memory to re use it. Created a file lib/dbConnect.js

import mongoose from 'mongoose';

const MONGODB_URI = process.env.MONGODB_URI;

if (!MONGODB_URI) {
  throw new Error(
    'Please define the MONGODB_URI environment variable inside .env.local'
  );
}

/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentially
 * during API Route usage.
 */
let cached = global.mongoose;

if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

async function dbConnect() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const opts = {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      bufferCommands: false,
      bufferMaxEntries: 0,
      useFindAndModify: false,
      useCreateIndex: true,
    };

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
      return mongoose;
    });
  }
  cached.conn = await cached.promise;
  return cached.conn;
}

export default dbConnect;

Enter fullscreen mode Exit fullscreen mode

Auth Code

For the authentication we need first to create a user! So here is the React function to accomplish it. You can ignore the components that have the actual html.

import React, { useState } from 'react';
import { useRouter } from 'next/router';
import SignUpLayout from '@/components/SignUp/SignUpLayout';
import Form from '@/components/SignUp/Form';
import fetchJson from '@/lib/fetchJson';

export default function Register() {
  const router = useRouter();
  const [errorMsg, setErrorMsg] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    const email = e.currentTarget.email.value;
    const password = e.currentTarget.password.value;
    const name = e.currentTarget.name.value;

    try {
      await fetchJson('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password, name }),
      });
      return router.push('/index');
    } catch (error) {
      setErrorMsg(error.data.message);
    }
  };
  return (
    <SignUpLayout>
      <Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit}></Form>
    </SignUpLayout>
  );
}


Enter fullscreen mode Exit fullscreen mode

Now let's configure the session handler/guard. This is inside lib/session.js and this is the configuration that iron session will use to create the cookie.

// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import { withIronSession } from 'next-iron-session'

export default function withSession(handler) {
  return withIronSession(handler, {
    password: process.env.SECRET_COOKIE_PASSWORD,
    cookieName: 'cookie-name',
    cookieOptions: {
      // the next line allows to use the session in non-https environments like
      // Next.js dev mode (http://localhost:3000)
      maxAge: 60 * 60 * 24 * 30, // 30 days
      secure: process.env.NODE_ENV === 'production' ? true : false,
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Register

Now we can use the Next.js api inside pages/api/auth/register.js. This code includes de usage of the User model from mongoose, the database connection, and the user creation with the hashed password.

import dbConnect from '@/lib/dbConnect';
import User from '@/models/user.model';
import bcrypt from 'bcryptjs';
import httpStatus from 'http-status';
import withSession from '@/lib/session';

export default withSession(async (req, res) => {
  const { name, email, password } = await req.body;
  try {
    if (req.method === 'POST') {
      await dbConnect();
      const userCheck = await User.findOne({ email: email.toLowerCase() });
      if (userCheck) {
        return res.status(httpStatus.BAD_REQUEST).json({ message: 'User already exists' });
      }
      // create user
      const hashPassword = await bcrypt.hash(password, 10);
      const user = await new User({
        name,
        email,
        password: hashPassword,
      });
      await user.save();
      req.session.set('user', { id: user._id, email: user.email });
      await req.session.save();
      return res.status(httpStatus.OK).end();
    }
    return res.status(httpStatus.BAD_REQUEST).end();
  } catch (error) {
    console.log(error, error.message);
    const { response: fetchResponse } = error;
    res.status(fetchResponse?.status || 500).json(error.message);
  }
});

Enter fullscreen mode Exit fullscreen mode

Remember to set the user in the session request with only the required fields and not the whole user. We don't want to save the users' password in the cookie, right?

req.session.set('user', { id: user._id, email: user.email });
Enter fullscreen mode Exit fullscreen mode

You can remove the cookie creation if you don't want to login automatically the user after its creation.

Login

Now for the login, first we will be creating a custom hook, so we can check if the user is logged in and redirect if it is the case. So here is the hook. This hook also uses the useSWR that the official Next.js suggests to use.

import { useEffect } from 'react'
import Router from 'next/router'
import useSWR from 'swr'

export default function useUser({
  redirectTo = false,
  redirectIfFound = false,
} = {}) {
  const { data: user, mutate: mutateUser } = useSWR('/api/user')

  useEffect(() => {
    // if no redirect needed, just return (example: already on /dashboard)
    // if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
    if (!redirectTo || !user) return

    if (
      // If redirectTo is set, redirect if the user was not found.
      (redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
      // If redirectIfFound is also set, redirect if the user was found
      (redirectIfFound && user?.isLoggedIn)
    ) {
      Router.push(redirectTo)
    }
  }, [user, redirectIfFound, redirectTo])

  return { user, mutateUser }
}
Enter fullscreen mode Exit fullscreen mode

The hook queries the session saved in the request like this: (this is the pages/api/user.js).

import withSession from '@/lib/session';

export default withSession(async (req, res) => {
  const user = req.session.get('user');
  if (user) {
    res.json({
      isLoggedIn: true,
      ...user,
    });
  } else {
    res.json({
      isLoggedIn: false,
    });
  }
});

Enter fullscreen mode Exit fullscreen mode

Now we can proceed to the React Login function.


import { useState } from "react";
import Form from "@/components/Login/Form";
import LoginLayout from "@/components/Login/LoginLayout";
import useUser from '@/lib/useUser';
import fetchJson from '@/lib/fetchJson';

// layout for page

export default function Login() {

  const { mutateUser } = useUser({
    redirectTo: '/',
    redirectIfFound: true,
  });

  const [errorMsg, setErrorMsg] = useState('');

  async function handleSubmit(e) {
    e.preventDefault();

    const body = {
      email: e.currentTarget.email.value,
      password: e.currentTarget.password.value
    };

    try {
      mutateUser(
        await fetchJson('/api/auth/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        })
      );
    } catch (error) {
      console.error('An unexpected error happened:', error);
      setErrorMsg(error.data.message);
    }
  }
  return (
    <>
      <LoginLayout>
        <Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit}></Form>
      </LoginLayout>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

And here is the pages/api/auth/login.js API call that uses the mongoose connection and bcrypt validation.

import withSession from '@/lib/session';
import User from '@/models/user.model';
import bcrypt from 'bcryptjs';
import httpStatus from 'http-status';
import dbConnect from '@/lib/dbConnect';

export default withSession(async (req, res) => {
  const { email, password } = await req.body;
  try {
    await dbConnect();
    // get user from db
    const user = await User.findOne({ email: email.toLowerCase() });
    if (!user) {
      // password not valid
      return res.status(httpStatus.UNAUTHORIZED).json({ message: 'User does not exist'});
    }
    // compare hashed password
    const valid = await bcrypt.compare(password, user.password);
    // if the password is a match
    if (valid === true) {
      req.session.set('user', { id: user._id, email: user.email });
      await req.session.save();
      return res.json(user);
    } else {
      // password not valid
      return res.status(httpStatus.UNAUTHORIZED).json({ message: 'Invalid Password'});
    }
  } catch (error) {
    console.log(error);
    const { response: fetchResponse } = error;
    res.status(fetchResponse?.status || 500).json(error.data);
  }
});
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
xarmzon profile image
Adelola Kayode Samson

Thank you

Collapse
 
manentai profile image
Simone Gabbriellini

hi! this is really interesting, thanks! I was wondering if you have also a github repo so to check the missing pieces...

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

β­οΈπŸŽ€ JavaScript Visualized: Promises & Async/Await

async await