NextJs Auth Starter

N
andrewgbliss
7 months ago

This article will go over how to create a starter project in Next,js that will handle authentication.

. . .

Github repo

https://github.com/EntryLevelDeveloperTraining/nextjs-auth-starter

Getting Started

I will be using the Next.js Tailwind CSS starter project located here:

Now that we have this starter project we need to install some node packages. Open your terminal to install these packages:

npm i pg sequelize sequelize-cli dotenv
  • pg-This is the module to handle Postgres connection. Postgres is a database we can store user information to use for authentication.
  • sequelize-This module will help us create objects and run queries on our Postgres database.
  • sequelize-cli-This module will help us run migrations and seeders. Migrations are files we can run to create the structure of the database. Seeders are the default data we can use to populate our database.
  • dotenv-This module will enable us to use environment variables. It is good practice to store variable in an .env file and not store credentials in code.

Running Postgres

We will be using Docker to run our Postgres database so we have somewhere to store user credentials. You can use any database, the package we are using, Sequelize, has many other databases it can use. If you don't have Docker installed you can go here:

Docker for Windows

https://hub.docker.com/editions/community/docker-ce-desktop-windows/

Docker for Mac

https://hub.docker.com/editions/community/docker-ce-desktop-mac/

Docker for Ubuntu

https://docs.docker.com/engine/install/ubuntu/

Now that we have Docker installed we can easily start a Postgres database. First thing we need to is to create a .env file. A .env file means an Environment file. With this file we can setup our database credentials.

DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
POSTGRES_HOST=localhost
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432

Now we can create a docker-compose.yml file that will defined what type of services we want to run. For this starter we want to run a Postgres Database.

version: '3'

services:
  db:
    image: postgres:12.4
    ports:
      - '5432:5432'
    env_file:
      - .env
    volumes:
      - ./db:/db
      - db:/var/lib/postgresql/data

volumes:
  db:

This will setup a service called db with a postgres image. We will open port 5432 so applications can connect to it. We will reference our .env file, and setup volumes so we can save our data to our local file system.

Now to start the Postgres database service we must run a Docker command at the root of the project:

docker-compose up

It should show some output, if everything is okay and you see no errors then the database is ready to use.

Sequelize

Now we need to create a folder to store all of our Sequelize code. So let's create a folder called db at the root of our project. Then in that folder we will create another folder called config, then within that folder we will create a file called config.js. So the file structure will look like this:

db/config/config.js

Now let's add this code, which will define our database credentials and options:

const path = require('path');
require('dotenv').config({ path: path.join(process.cwd(), '..', '.env') });

module.exports = {
  development: {
    username: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    database: process.env.POSTGRES_DB,
    host: process.env.POSTGRES_HOST,
    port: process.env.POSTGRES_PORT,
    dialect: 'postgres',
    logging: false,
    define: {
      syncOnAssociation: false,
    },
    syncOnAssociation: false,
    sync: { force: false },
    seederStorage: 'sequelize',
  },
  test: {
    username: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    database: process.env.POSTGRES_DB + '_test',
    host: process.env.POSTGRES_HOST,
    port: process.env.POSTGRES_PORT,
    dialect: 'postgres',
    logging: false,
    define: {
      syncOnAssociation: false,
    },
    syncOnAssociation: false,
    sync: { force: false },
    seederStorage: 'sequelize',
  },
  production: {
    username: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    database: process.env.POSTGRES_DB,
    host: process.env.POSTGRES_HOST,
    port: process.env.POSTGRES_PORT,
    dialect: 'postgres',
    logging: false,
    define: {
      syncOnAssociation: false,
    },
    syncOnAssociation: false,
    sync: { force: false },
    seederStorage: 'sequelize',
    dialectOptions: {
      ssl: {
        rejectUnauthorized: false,
      },
    },
  },
};

This will use the dotenv node package we installed earlier. It will bring in the .env credentials and define them in this config object. It is bad practice to store database credentials, hard codeded, in the file so we will use the .env.

Migrations

Now we will create some database migrations. Under the db folder create a folder called migrations. Now let's create a migration that will create a users table in the database.

db/migrations/20210101100000-create-users.js

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER,
      },
      email: {
        allowNull: false,
        type: Sequelize.STRING,
        unique: true,
      },
      password: {
        allowNull: false,
        type: Sequelize.STRING,
      },
      username: {
        allowNull: true,
        type: Sequelize.STRING,
        unique: true,
      },
      firstName: {
        allowNull: true,
        type: Sequelize.STRING,
      },
      lastName: {
        allowNull: true,
        type: Sequelize.STRING,
      },
      verified: {
        allowNull: false,
        type: Sequelize.BOOLEAN,
        defaultValue: false,
      },
      verificationHash: {
        allowNull: true,
        type: Sequelize.STRING,
      },
      resetPasswordHash: {
        allowNull: true,
        type: Sequelize.STRING,
      },
      resetPasswordTimeout: {
        allowNull: true,
        type: Sequelize.DATE,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      deletedAt: {
        allowNull: true,
        type: Sequelize.DATE,
      },
    });
  },
  down: (queryInterface) => {
    return queryInterface.dropTable('users');
  },
};

This will create a table that has these columns. The most important columns we will be using are email and password. When a user wants to login we will check those two columns to make sure they are valid credentials, if they are not it show a message that says not valid.

Add these npm scripts to your package.json so we can easily run sequelize commands:

"sequelize": "cd ./db && sequelize",
"migrate": "npm run sequelize db:migrate",
"seed": "npm run sequelize db:seed:all",

Now you can run this command to run the migration:

npm run migrate

Model

We now need to create a Model. A model is an data definition object that we can use, when working in code, that will take data from the database and populate the object. Then we can use that object to perform actions, like create more objects, update and delete.

Before we create our first model let's also install some more npm packages that we can use to encrypt passwords and create password timeouts.

npm i bcryptjs moment

Let's create a models folder and our first model:

db/models/Users.ts

import { Model, DataTypes } from 'sequelize';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import moment from 'moment';
import restoreAttributes from '../utils/restoreAttributes';

export default class Users extends Model {
  public id!: number;
  public email!: string;
  public password!: string;
  public username!: string;
  public firstName!: string;
  public lastName!: string;
  public verified!: boolean;
  public resetPasswordHash!: string;
  public resetPasswordTimeout!: string;
  public verificationHash!: string;
  public createdAt!: Date;
  public updatedAt!: Date;
  public deletedAt!: Date;

  constructor(...args) {
    super(...args);
    restoreAttributes(new.target, this);
  }

  static associate(models) {}

  hashPassword() {
    this.password = bcrypt.hashSync(this.password);
  }

  setResetPassword() {
    const timeout = 15;
    const today = new Date();
    const twoweeks = new Date(
      today.getFullYear(),
      today.getMonth(),
      today.getDate() + 14
    );
    const str = this.email + twoweeks.toISOString();
    this.resetPasswordHash = crypto.createHash('md5').update(str).digest('hex');
    this.resetPasswordTimeout = moment()
      .add(Number(timeout), 'minutes')
      .format();
  }

  unsetResetPassword() {
    this.resetPasswordHash = null;
    this.resetPasswordTimeout = null;
  }

  hasValidResetPassword() {
    return moment().isBefore(this.resetPasswordTimeout);
  }

  verifyPassword(password: string) {
    return bcrypt.compareSync(password, this.password);
  }
}

export const UsersFactory = (sequelize) => {
  Users.init(
    {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
      email: {
        allowNull: false,
        type: DataTypes.STRING,
        validate: {
          isEmail: true,
        },
        unique: true,
      },
      password: {
        allowNull: false,
        type: DataTypes.STRING,
      },
      username: {
        allowNull: true,
        type: DataTypes.STRING,
        unique: true,
      },
      firstName: {
        allowNull: true,
        type: DataTypes.STRING,
      },
      lastName: {
        allowNull: true,
        type: DataTypes.STRING,
      },
      verified: {
        allowNull: false,
        type: DataTypes.BOOLEAN,
        defaultValue: false,
      },
      verificationHash: {
        allowNull: true,
        type: DataTypes.STRING,
      },
      resetPasswordHash: {
        allowNull: true,
        type: DataTypes.STRING,
      },
      resetPasswordTimeout: {
        allowNull: true,
        type: DataTypes.DATE,
      },
      createdAt: {
        allowNull: false,
        type: DataTypes.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: DataTypes.DATE,
      },
      deletedAt: {
        allowNull: true,
        type: DataTypes.DATE,
      },
    },
    {
      tableName: 'users',
      sequelize,
      paranoid: true,
      hooks: {
        beforeUpdate(user: Users) {
          if (user.changed('password')) {
            user.hashPassword();
          }
        },
        beforeCreate(user: Users) {
          user.hashPassword();
          const today = new Date();
          const nextweek = new Date(
            today.getFullYear(),
            today.getMonth(),
            today.getDate() + 7
          );
          const str = this.email + nextweek.toISOString();
          user.verificationHash = crypto
            .createHash('md5')
            .update(str)
            .digest('hex');
        },
      },
    }
  );

  return Users;
};

This model will do a lot of things for us. First is we setup to use Typescript so we defined the columns at the top of the model. Then we setup some helper functions.

  • hashPassword-This will encrypt the password. So if someone were to get access to the database they don't see plain text passwords.
  • setResetPassword-This will will set a special hash token that can be used to reset a password and a timeout so when the user requests to reset their password they are the only ones that reset the password within a time limit.
  • hasValidResetPassword-This will check if the user is within the time limit to reset the password
  • verifyPassword-This will check if the users password is correct

This model will also setup some hooks. Hooks are run every time a certain an action has occurred. For this model we will setup two hooks:

  • beforeCreate-This will encrypt the users password and setup a verification token. We will then send an email to the user when they first register, then we the user gets the email and clicks on Verify we can set a boolean to say whether or not the email address is a valid email and thus a valid user.
  • beforeUpdate-When the user updates the password we will run it through the encrypt password function.

Now let's create an index.ts file and export our model so we can use it:

db/models/index.ts

import { Sequelize } from 'sequelize';
import { UsersFactory } from './Users';
const config = require('../config/config');

const env = process.env.NODE_ENV;
const configEnv = config[env];

const sequelize = new Sequelize(process.env.DATABASE_URL, configEnv);

const models = {
  Users: UsersFactory(sequelize),
};

Object.values(models).forEach((model) => model.associate(models));

const db = {
  ...models,
  sequelize,
  Sequelize,
};

(async () => {
  try {
    await sequelize.authenticate();
  } catch (error) {
    console.error('Unable to connect to the database:', error);
  }
})();

export default db;

This will create a connection to the database using our .env environment variables, create our database models, and give us one nice default export we can use anywhere in our code.

We also need to create this file to use because of Typescript transpiling:

db/utils/restoreAttributes.ts

import { Model } from 'sequelize';

/**
 * Because of https://github.com/sequelize/sequelize/issues/10579, the classes transpiled
 * by Babel are resetting the sequelize properties getters & setters. This helper fixes these properties
 * by re-defining them. Based on https://github.com/RobinBuschmann/sequelize-typescript/issues/612#issuecomment-491890977.
 *
 * Must be called from the constructor.
 */
export default function restoreAttributes(newTarget, self: Model): void {
  Object.keys(newTarget.rawAttributes).forEach((propertyKey: keyof Model) => {
    Object.defineProperty(self, propertyKey, {
      get() {
        return self.getDataValue(propertyKey);
      },
      set(value) {
        self.setDataValue(propertyKey, value);
      },
    });
  });
}

Seeders

A seeder file is data that we can use to seed the database, or fill it with data. Let's create a users seeders file:

db/seeders/20210101100000-users.js

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert(
      'users',
      [
        {
          email: 'test@test.com',
          password:
            '$2a$10$onYlRIHDNLs4a0mb0ft.8uSl1hjW1ThoZVuyEh3LdBPzGrcGZKEzu',
          username: 'test',
          firstName: 'Test',
          lastName: 'Test',
          verified: true,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ],
      {}
    );
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('users', null, {});
  },
};

Now to run this go to the terminal and run this command:

npm run seed

This will create a user with an email of test@test.com and password that is already encrypted that is abc123.

Middleware

Now we have everything setup for our database. Let's create some middleware so we can easily check for a valid user and run sequelize functions.

Let's create a folder called lib and under that a folder called middleware.

Run Middleware

This will help us run middleware that is made for ExpressJs.

lib/middleware/runMiddleware.ts

// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result);
      }

      return resolve(result);
    });
  });
}

export default runMiddleware;

Require JWT

This middleware will use the express-jwt package. JWT stands for JSON web token and will be our primary method for authentication. If we use this on an endpoint, such as changing a password, then we must require the user has a JWT. We will also bring in the environment variable JWT_SECRET, which you can add to the .env as any random string of characters.

Need to install the package:

npm i express-jwt
import expressjwt from 'express-jwt';
import runMiddleware from './runMiddleware';
const JWT_SECRET = process.env.JWT_SECRET;

const requireJwt = async (req, res) => {
  await runMiddleware(
    req,
    res,
    expressjwt({
      algorithms: ['HS256'],
      secret: JWT_SECRET,
      credentialsRequired: false,
      getToken(req) {
        if (
          req.headers.authorization &&
          req.headers.authorization.split(' ')[0] === 'Bearer'
        ) {
          return req.headers.authorization.split(' ')[1];
        } else if (req.query && req.query.token) {
          return req.query.token;
        }
        return null;
      },
    })
  );
};

export default requireJwt;

Async Endpoint

This middleware will help us catch errors in async endpoints.

lib/middleware/asyncEndpoint.ts

const asyncEndpoint = async (req, res, endpoint) => {
  try {
    return await endpoint(req, res);
  } catch (e) {
    console.error(e);
    if (e.name === 'SequelizeUniqueConstraintError') {
      if (e.errors) {
        let messages = e.errors.map((i) => i.message);
        let errMessage = `Validation errors: ${messages.join(', ')}`;
        res.status(400).json({ error: { message: errMessage } });
      }
    } else {
      res.status(e.status || 500).json({ error: { message: e.message } });
    }
  }
};

export default asyncEndpoint;

If it catches a Sequelize Error it will join all the messages as one so we can show the user.

Refresh JWT

This middleware will generate the JWT for us based on the user id that logs in. We also need to install the package:

npm i jsonwebtoken

lib/middleware/refreshJwt.ts

import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET;
const SESSION_TIMEOUT = 3600 * 24; // 24 hours

export default function refreshJwt(res, id: number) {
  const token = jwt.sign(
    {
      id,
      createdAt: new Date(),
    },
    JWT_SECRET,
    {
      expiresIn: SESSION_TIMEOUT,
    }
  );
  res.setHeader('JWT_TOKEN', token);
}

Logged In User

This middleware will run a query based on the JWT and get the logged in users profile.

lib/middleware/loggedInUser.ts

import db from '@db/models';

export default async function loggedInUser(req) {
  if (!req?.user?.id) {
    req.user = null;
    return;
  }
  req.user = await db.Users.findByPk(req.user.id, {
    attributes: [
      'id',
      'email',
      'username',
      'firstName',
      'lastName',
      'createdAt',
      'updatedAt',
    ],
    raw: true,
  });
}

Require User

This middleware will check to see if there is a valid user. If not it will throw an error.

lib/middleware/requireUser.ts

const requireUser = (req) => {
  if (!req.user) {
    throw {
      status: 401,
      message: 'Invalid authentication',
    };
  }
};

export default requireUser;

toJson

This middleware will return the JSON results that we setup with a 200 status.

lib/middleware/toJson.ts

const toJson = (req, res) => {
  res.status(200).json(req.results);
};

export default toJson;

Validate Schema

This will take in a schema and check to make sure we are sending our endpoints correct values.

We also need to install this package:

npm i joi

lib/middleware/validateSchema.ts

const validateSchema = (data, schema) => {
  if (!data) {
    throw {
      status: 400,
      message: 'No data was sent',
    };
  }
  let validation = schema.validate(data, {
    abortEarly: false,
  });
  if (validation.error) {
    let messages = validation.error.details.map((i) => i.message);
    let errMessage = `Validation errors: ${messages.join(', ')}`;
    throw {
      status: 400,
      message: errMessage,
    };
  }
};

export default validateSchema;

Sequelize

This middleware will help us set up some dynamic endpoints.

We also need to install this npm package:

npm i lodash

lib/middleware/sequelize.ts

import get from 'lodash/get';
import isString from 'lodash/isString';
import isArray from 'lodash/isArray';
import omit from 'lodash/omit';
import qs from 'querystring';
import db from '../../db';
import toJson from './toJson';

interface SequelizeOptions {
  offset: number;
  limit: number;
  order?: Array<any>;
  attributes?: Array<string>;
  include?: Array<any>;
  logging?: any;
}

interface Include {
  model: string;
  as?: string;
  attributes?: string;
  where?: any;
}

export const withSequelize = (props) => {
  const { req } = props;
  const { Sequelize } = db;
  const {
    page = 0,
    limit = 100,
    order = '',
    attributes = ['id'],
    include,
  }: any = req.query;
  let options: SequelizeOptions = {
    offset: page === 0 ? 0 : parseInt(page) * parseInt(limit),
    limit: parseInt(limit),
  };
  let conditions = {};
  if (order && isString(order)) {
    const [column, direction = 'ASC'] = order.split(',');
    options.order = [[Sequelize.col(column), direction]];
  } else if (order && isArray(order)) {
    options.order = order.map((orderGroup = '') => {
      const [column, direction = 'ASC'] = orderGroup.split(',');
      return [Sequelize.col(column), direction];
    });
  }
  if (attributes && isString(attributes)) {
    options.attributes = attributes.split(',');
  } else if (attributes && isArray(attributes)) {
    options.attributes = attributes;
  }
  if (attributes && isString(attributes)) {
    options.attributes = attributes.split(',');
  } else if (attributes && isArray(attributes)) {
    options.attributes = attributes;
  }
  if (include && isArray(include)) {
    options.include = include.map((includeModel) => {
      const { model, as, attributes, ...rest }: any = qs.parse(
        includeModel,
        ';',
        '='
      );
      const include: Include = {
        model: db[model],
      };
      if (as) {
        include.as = as;
      }
      if (attributes) {
        include.attributes = attributes.split(',');
      }
      const otherColumns = omit(rest, [
        'page',
        'limit',
        'order',
        'attributes',
        'include',
      ]);
      if (otherColumns) {
        include.where = otherColumns;
      }
      return include;
    });
  }
  const otherColumns = omit(req.query, [
    'page',
    'limit',
    'order',
    'attributes',
    'include',
  ]);
  if (otherColumns) {
    conditions = {
      where: otherColumns,
    };
  }
  req.sequelize = {
    options,
    conditions,
  };
};

export const create = async (props) => {
  const { req, res, model } = props;
  const dbModel = db[model];
  if (!dbModel) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  req.results = await dbModel.create(req.body);
  toJson(req, res);
};

export const bulkCreate = async (props) => {
  const { req, res, model, path } = props;
  const dbModel = db[model];
  if (!dbModel) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  const data = get(req.body, path);
  req.results = await dbModel.bulkCreate(data, { individualHooks: true });
  toJson(req, res);
};

export const read = async (props) => {
  const { req, res, model } = props;
  const dbModel = db[model];
  if (!dbModel) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  withSequelize(props);
  req.results = await dbModel.findAll({
    ...req.sequelize.conditions,
    ...req.sequelize.options,
  });
  toJson(req, res);
};

export const findByPk = async (props) => {
  const { req, res, model, id } = props;
  const dbModel = db[model];
  if (!dbModel) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  withSequelize(props);
  req.results = await dbModel.findByPk(req.query[id], {
    ...req.sequelize.conditions,
    ...req.sequelize.options,
  });
  toJson(req, res);
};

export const findOne = async (props) => {
  const { req, res, model } = props;
  const dbModel = db[model];
  if (!model) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  withSequelize(props);
  req.sequelize.conditions.where = {
    ...req.sequelize.conditions.where,
  };
  req.results = await dbModel.findOne({
    ...req.sequelize.conditions,
    ...req.sequelize.options,
  });
  toJson(req, res);
};

export const update = async (props) => {
  const { req, res, model, key, path, fields } = props;
  const dbModel = db[model];
  if (!dbModel) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  req.results = await dbModel.update(req.body, {
    where: {
      [key]: get(req, path),
    },
    fields,
  });
  toJson(req, res);
};

export const destroy = async (props) => {
  const { req, res, model, key, path } = props;
  const dbModel = db[model];
  if (!dbModel) {
    throw {
      status: 404,
      message: 'Model not found',
    };
  }
  req.results = await dbModel.destroy({
    where: {
      [key]: get(req, path),
    },
  });
  toJson(req, res);
};

Sequelize Router

This middleware will use the other middleware we have created and create for us a bunch of different endpoints.

lib/middleware/sequelizeRouter.ts

import validateSchema from './validateSchema';
import asyncEndpoint from './asyncEndpoint';
import { create, read, findByPk, update, destroy } from './sequelize';

export const sequelizeRouterIndex = async (props) => {
  const { req, res, model, schemas } = props;
  if (req.method === 'GET') {
    await asyncEndpoint(req, res, async () => read({ req, res, model }));
  } else if (req.method === 'POST') {
    await validateSchema(req.body, schemas.create);
    await asyncEndpoint(req, res, async () => create({ req, res, model }));
  } else {
    res.status(404).json({ error: { message: 'Not found' } });
  }
};

export const sequelizeRouterResource = async (props) => {
  const { req, res, model, key = 'id', schemas } = props;
  if (req.method === 'GET') {
    await asyncEndpoint(req, res, async () =>
      findByPk({ req, res, model, id: `id` })
    );
  } else if (req.method === 'PUT') {
    await validateSchema(req.body, schemas.update);
    await asyncEndpoint(req, res, async () =>
      update({
        req,
        res,
        model,
        key,
        path: `query.id`,
      })
    );
  } else if (req.method === 'DELETE') {
    await asyncEndpoint(req, res, async () =>
      destroy({
        req,
        res,
        model,
        key,
        path: `query.id`,
      })
    );
  } else {
    res.status(404).json({ error: { message: 'Not found' } });
  }
};

Now we have setup a ton of middleware we can easily write endpoints.

Login Endpoints

Now let's create a login endpoint we can use to check the database, check credentials, and if they are correct it will send back a JWT they can use to make other calls.

pages/api/v1/login/index.ts

import db from '@db/models';
import { validateSchema, asyncEndpoint, refreshJwt } from '@lib/middleware';
import joi from 'joi';

const loginSchema = joi.object().keys({
  email: joi.string().required(),
  password: joi.string().required(),
});

const login = async (req, res) => {
  validateSchema(req.body, loginSchema);
  const user = await db.Users.findOne({
    attributes: ['id', 'email', 'password', 'verified'],
    where: {
      email: req.body.email,
    },
  });
  if (!user || !user.verifyPassword(req.body.password)) {
    throw {
      status: 403,
      message: 'Email or Password is incorrect',
    };
  }
  if (!user.verified) {
    throw {
      status: 403,
      message:
        'Please verify your account by following the link in the registration email that was sent.',
    };
  }
  refreshJwt(res, user.id);
  res.status(200).json({ message: 'success' });
};

const route = async (req, res) => {
  if (req.method === 'POST') {
    await asyncEndpoint(req, res, login);
  } else {
    res.status(404).json({ error: { message: 'Not found' } });
  }
};

export default route;

Conclusion

We create migrations and models for our database. We created middleware to handle authentication using a JWT. And we have setup endpoints to login. You can read the entire repo and use it as a starter project. It also has login forms, forgot password, and more.


N
andrewgbliss
7 months ago