Published: September 08 2020

Node.js + MySQL - Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password

Tutorial built with Node.js, Sequelize and MySQL

Other versions available:

In this tutorial we'll cover how to build a boilerplate sign up and authentication API with Node.js and MySQL that includes:

  • Email sign up and verification
  • JWT authentication with refresh tokens
  • Role based authorization with support for two roles (User & Admin)
  • Forgot password and reset password functionality
  • Account management (CRUD) routes with role based access control
  • Swagger api documentation route


Tutorial Contents

The tutorial is organised into the following main sections:


Node + MySQL Boilerplate Overview

There are no users registered in the node.js boilerplate api by default, in order to authenticate you must first register and verify an account. The api sends a verification email after registration with a token to verify the account. Email SMTP settings must be set in the config.json file for email to work correctly, you can create a free test account in one click at https://ethereal.email/ and copy the options below the title Nodemailer configuration.

The first account registered is assigned to the Admin role and subsequent accounts are assigned to the regular User role. Admins have full access to CRUD routes for managing all accounts, while regular users can only modify their own account.

JWT authentication with refresh tokens

Authentication is implemented with JWT access tokens and refresh tokens. On successful authentication the boilerplate api returns a short lived JWT access token that expires after 15 minutes, and a refresh token that expires after 7 days in a HTTP Only cookie. The JWT is used for accessing secure routes on the api and the refresh token is used for generating new JWT access tokens when (or just before) they expire. HTTP Only cookies are used for increased security because they are not accessible to client-side javascript which prevents XSS (cross site scripting), and the refresh token can only be used to fetch a new JWT token from the /accounts/refresh-token route which prevents CSRF (cross site request forgery).

Refresh token rotation

As an added security measure in the refreshToken() method of the account service, each time a refresh token is used to generate a new JWT token, the refresh token is revoked and replaced by a new refresh token. This technique is known as Refresh Token Rotation and increases security by reducing the lifetime of refresh tokens, which makes it less likely that a compromised token will be valid (or valid for long). When a refresh token is rotated the new token is saved in the replacedByToken property of the revoked token to create an audit trail in the MySQL database.

Code on GitHub

The boilerplate api project is available on GitHub at https://github.com/cornflourblue/node-mysql-signup-verification-api.


Run the Node + MySQL Boilerplate API Locally

  1. Install NodeJS and NPM from  https://nodejs.org/en/download/.
  2. Install MySQL Community Server from https://dev.mysql.com/downloads/mysql/ and ensure it is started. Installation instructions are available at https://dev.mysql.com/doc/refman/8.0/en/installing.html.
  3. Download or clone the project source code from https://github.com/cornflourblue/node-mysql-signup-verification-api
  4. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  5. Configure SMTP settings for email within the smtpOptions property in the /src/config.json file. For testing you can create a free account in one click at https://ethereal.email/ and copy the options below the title Nodemailer configuration.
  6. Start the api by running npm start (or npm run start:dev to start with nodemon) from the command line in the project root folder, you should see the message Server listening on port 4000, and you can view the Swagger API documentation at http://localhost:4000/api-docs.
  7. Follow the instructions below to test with Postman or hook up with an example React or Angular application.

Before running in production

Before running in production also make sure that you update the secret property in the config.json file, it is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).


Run a React App with the Node + MySQL Boilerplate API

For full details about the boilerplate React app see the post React Boilerplate - Email Sign Up with Verification, Authentication & Forgot Password. But to get up and running quickly just follow the below steps.

  1. Download or clone the React tutorial code from https://github.com/cornflourblue/react-signup-verification-boilerplate
  2. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  3. Remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.jsx file.
  4. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the Node + MySQL Boilerplate API that you already have running.


Run an Angular 10 App with the Node + MySQL Boilerplate API

For full details about the boilerplate Angular 10 app see the post Angular 10 Boilerplate - Email Sign Up with Verification, Authentication & Forgot Password. But to get up and running quickly just follow the below steps.

  1. Download or clone the Angular 10 tutorial code from https://github.com/cornflourblue/angular-10-signup-verification-boilerplate
  2. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  3. Remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file.
  4. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the Node.js + MySQL Boilerplate API that you already have running.


Test the Node.js Boilerplate API with Postman

Postman is a great tool for testing APIs, you can download it at https://www.getpostman.com/.

Below are instructions on how to use Postman to:


How to register a new account with Postman

To register a new account with the Node.js boilerplate api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the register route of your local API - http://localhost:4000/accounts/register
  4. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  5. Enter a JSON object containing the required user properties in the "Body" textarea, e.g:
    {
        "title": "Mr",
        "firstName": "Jason",
        "lastName": "Watmore",
        "email": "[email protected]",
        "password": "my-super-secret-password",
        "confirmPassword": "my-super-secret-password",
        "acceptTerms": true
    }
  6. Click the "Send" button, you should receive a "200 OK" response with a "registration successful" message in the response body.

Here's a screenshot of Postman after the request is sent and the new user has been registered:

And this is a screenshot of the verification email received with the token to verify the account:

Back to top


How to verify an account with Postman

To verify an account with the Node api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/accounts/verify-email
  4. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  5. Enter a JSON object containing the token received in the verification email (in the previous step) in the "Body" textarea, e.g:
    {
        "token": "REPLACE THIS WITH YOUR TOKEN"
    }
  6. Click the "Send" button, you should receive a "200 OK" response with a "verification successful" message in the response body.

Here's a screenshot of Postman after the request is sent and the account has been authenticated:

Back to top


How to access an account if you forgot the password

To re-enable access to an account with a forgotten password you need to submit the email address of the account to the /account/forgot-password route, the route will then send a token to the email which will allow you to reset the password of the account in the next step.

Follow these steps in Postman if you forgot the password for an account:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/accounts/forgot-password
  4. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  5. Enter a JSON object containing the email of the account with the forgotten password in the "Body" textarea, e.g:
    {
        "email": "[email protected]"
    }
  6. Click the "Send" button, you should receive a "200 OK" response with the message "Please check your email for password reset instructions" in the response body.

Here's a screenshot of Postman after the request is sent and the email has been sent:

And this is a screenshot of the email received with the token to reset the password of the account:

Back to top


How to reset the password of an account with Postman

To reset the password of an account with the api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/accounts/reset-password
  4. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  5. Enter a JSON object containing the password reset token received in the email from the forgot password step, along with a new password and matching confirmPassword, into the "Body" textarea, e.g:
    {
        "token": "REPLACE THIS WITH YOUR TOKEN",
        "password": "new-super-secret-password",
        "confirmPassword": "new-super-secret-password"
    }
  6. Click the "Send" button, you should receive a "200 OK" response with a "password reset successful" message in the response body.

Here's a screenshot of Postman after the request is sent and the account password has been reset:

Back to top


How to authenticate with Postman

To authenticate an account with the api and get a JWT token follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/accounts/authenticate
  4. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  5. Enter a JSON object containing the account email and password in the "Body" textarea:
    {
        "email": "[email protected]",
        "password": "my-super-secret-password"
    }
  6. Click the "Send" button, you should receive a "200 OK" response with the user details including a JWT token in the response body and a refresh token in the response cookies.
  7. Copy the JWT token value because we'll be using it in the next steps to make authenticated requests.

Here's a screenshot of Postman after the request is sent and the account has been authenticated:

And this is the response cookies tab with the refresh token:

Back to top


How to get a list of all accounts with Postman

This is a secure request that requires a JWT authentication token from the authenticate step. The api route is restricted to admin users.

To get a list of all accounts from the Node boilerplate api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the users route of your local API - http://localhost:4000/accounts
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate step into the "Token" field.
  5. Click the "Send" button, you should receive a "200 OK" response containing a JSON array with all of the account records in the system.

Here's a screenshot of Postman after making an authenticated request to get all accounts:

Back to top


How to update an account with Postman

This is a secure request that requires a JWT authentication token from the authenticate step. Admin users can update any account including its role, while regular users are restricted to their own account and cannot update role. Omitted or empty properties are not updated.

To update an account with the api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "PUT" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the /accounts/{id} route with the id of the account you want to update, e.g - http://localhost:4000/accounts/1
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate step into the "Token" field.
  5. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  6. Enter a JSON object in the "Body" textarea containing the properties you want to update, for example to update the first and last names:
    {
        "firstName": "Frank",
        "lastName": "Murphy"
    }
  7. Click the "Send" button, you should receive a "200 OK" response with the updated account details in the response body.

Here's a screenshot of Postman after the request is sent and the account has been updated:

Back to top


How to use a refresh token to get a new JWT token

This step can only be done after the authenticate step because a valid refresh token cookie is required.

To use a refresh token cookie to get a new JWT token and a new refresh token follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the refresh token route of your local API - http://localhost:4000/accounts/refresh-token
  4. Click the "Send" button, you should receive a "200 OK" response with the account details including a new JWT token in the response body and a new refresh token in the response cookies.
  5. Copy the JWT token value because we'll be using it in the next steps to make authenticated requests.

Here's a screenshot of Postman after the request is sent and the token has been refreshed:

And this is the response cookies tab with the new refresh token:

Back to top


How to revoke a refresh token with Postman

This is a secure request that requires a JWT authentication token from the authenticate (or refresh token) step. Admin users can revoke the tokens of any account, while regular users can only revoke their own tokens.

To revoke a refresh token so it can no longer be used to generate JWT tokens, follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/accounts/revoke-token
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate (or refresh token) step into the "Token" field.
  5. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  6. Enter a JSON object containing the active refresh token from the previous step in the "Body" textarea, e.g:
    {
        "token": "ENTER THE ACTIVE REFRESH TOKEN HERE"
    }
  7. Click the "Send" button, you should receive a "200 OK" response with the message Token revoked.

Note: You can also revoke the token in the refreshToken cookie with the /accounts/revoke-token route, to revoke the refresh token cookie simply send the same request with an empty body.

Here's a screenshot of Postman after making the request and the token has been revoked:

Back to top


How to delete an account with Postman

This is a secure request that requires a JWT authentication token from the authenticate step. Admin users can delete any account, while regular users are restricted to their own account.

To delete an account with the api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "DELETE" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the /accounts/{id} route with the id of the account you want to delete, e.g - http://localhost:4000/accounts/1
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate step into the "Token" field.
  5. Click the "Send" button, you should receive a "200 OK" response with the message "Account deleted successfully" in the response body.

Here's a screenshot of Postman after the request is sent and the account has been deleted:

Back to top


Node.js + MySQL Boilerplate API Project Structure

The tutorial project is structured into feature folders (accounts) and non-feature / shared component folders (_helpers, _middleware). Shared component folders contain code that can be used by multiple features and other parts of the application, and are prefixed with an underscore _ to group them together and make it easy to differentiate between feature folders and non-feature folders.

The boilerplate example only contains a single (accounts) feature at the moment, but could be easily extended with other features by copying the accounts folder and following the same pattern.

Click any of the below links to jump down to a description of each file along with its code:

 

Helpers Folder

Path: /_helpers

The helpers folder contains all the bits and pieces that don't fit into other folders but don't justify having a folder of their own.

 

MySQL Database Wrapper

Path: /_helpers/db.js

The MySQL database wrapper connects to MySQL using Sequelize and the MySQL2 client, and exports an object containing all of the database model objects in the application (currently only Account and RefreshToken). It provides an easy way to access any part of the database from a single point.

The initialize() function is executed once on api startup and performs the following actions:

  • Connects to MySQL server using the mysql2 db client and executes a query to create the database if it doesn't already exist.
  • Connects to the database with the Sequelize ORM.
  • Initializes the Account and RefreshToken models and attaches them to the exported db object.
  • Defines the one-to-many relationship between accounts and refresh tokens and configures refresh tokens to be deleted when the account they belong to is deleted.
  • Automatically creates tables in MySQL database if they don't exist by calling await sequelize.sync(). For more info on Sequelize model synchronization options see https://sequelize.org/master/manual/model-basics.html#model-synchronization.
const config = require('config.json');
const mysql = require('mysql2/promise');
const { Sequelize } = require('sequelize');

module.exports = db = {};

initialize();

async function initialize() {
    // create db if it doesn't already exist
    const { host, port, user, password, database } = config.database;
    const connection = await mysql.createConnection({ host, port, user, password });
    await connection.query(`CREATE DATABASE IF NOT EXISTS \`${database}\`;`);

    // connect to db
    const sequelize = new Sequelize(database, user, password, { dialect: 'mysql' });

    // init models and add them to the exported db object
    db.Account = require('../accounts/account.model')(sequelize);
    db.RefreshToken = require('../accounts/refresh-token.model')(sequelize);

    // define relationships
    db.Account.hasMany(db.RefreshToken, { onDelete: 'CASCADE' });
    db.RefreshToken.belongsTo(db.Account);
    
    // sync all models with database
    await sequelize.sync();
}
 

Role Object / Enum

Path: /_helpers/role.js

The role object defines the all the roles in the example application, I created it to use like an enum to avoid passing roles around as strings, so instead of 'Admin' and 'User' we can use Role.Admin and Role.User.

module.exports = {
    Admin: 'Admin',
    User: 'User'
}
 

Send Email Helper

Path: /_helpers/send-email.js

The send email helper is a lightweight wrapper around the nodemailer module to simplify sending emails from anywhere in the application. It is used by the account service to send account verification and password reset emails.

const nodemailer = require('nodemailer');
const config = require('config.json');

module.exports = sendEmail;

async function sendEmail({ to, subject, html, from = config.emailFrom }) {
    const transporter = nodemailer.createTransport(config.smtpOptions);
    await transporter.sendMail({ from, to, subject, html });
}
 

Swagger API Docs Route Handler (/api-docs)

Path: /_helpers/swagger.js

The Swagger docs route handler uses the Swagger UI Express module to serve auto-generated Swagger UI documentation based on the swagger.yaml file from the /api-docs path of the api. The route handler is bound to the /api-docs path in the main server.js file.

For more info on swagger-ui-express see https://www.npmjs.com/package/swagger-ui-express.

const express = require('express');
const router = express.Router();
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const swaggerDocument = YAML.load('./swagger.yaml');

router.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

module.exports = router;
 

Express.js Middleware Folder

Path: /_middleware

The middleware folder contains Express.js middleware functions that can be used by different routes / features within the Node.js boilerplate api.

 

Authorize Middleware

Path: /_middleware/authorize.js

The authorize middleware can be added to any route to restrict access to the route to authenticated users with specified roles. If the roles parameter is omitted (i.e. authorize()) then the route will be accessible to all authenticated users regardless of role. It is used by the accounts controller to restrict access to account CRUD routes and revoke token route.

The authorize function returns an array containing two middleware functions:

  • The first (jwt({ ... })) authenticates the request by validating the JWT access token in the "Authorization" header of the http request. On successful authentication a user object is attached to the req object that contains the data from the JWT token, which in this example includes the user id (req.user.id).
  • The second authorizes the request by checking that the authenticated account still exists and is authorized to access the requested route based on its role. The second middleware function also attaches the role property and the ownsToken method to the req.user object so they can be accessed by controller functions.

If either authentication or authorization fails then a 401 Unauthorized response is returned.

const jwt = require('express-jwt');
const { secret } = require('config.json');
const db = require('_helpers/db');

module.exports = authorize;

function authorize(roles = []) {
    // roles param can be a single role string (e.g. Role.User or 'User') 
    // or an array of roles (e.g. [Role.Admin, Role.User] or ['Admin', 'User'])
    if (typeof roles === 'string') {
        roles = [roles];
    }

    return [
        // authenticate JWT token and attach user to request object (req.user)
        jwt({ secret, algorithms: ['HS256'] }),

        // authorize based on user role
        async (req, res, next) => {
            const account = await db.Account.findByPk(req.user.id);

            if (!account || (roles.length && !roles.includes(account.role))) {
                // account no longer exists or role not authorized
                return res.status(401).json({ message: 'Unauthorized' });
            }

            // authentication and authorization successful
            req.user.role = account.role;
            const refreshTokens = await account.getRefreshTokens();
            req.user.ownsToken = token => !!refreshTokens.find(x => x.token === token);
            next();
        }
    ];
}
 

Global Error Handler Middleware

Path: /_middleware/error-handler.js

The global error handler is used catch all errors and remove the need for duplicated error handling code throughout the boilerplate application. It's configured as middleware in the main server.js file.

By convention errors of type 'string' are treated as custom (app specific) errors, this simplifies the code for throwing custom errors since only a string needs to be thrown (e.g. throw 'Invalid token'). Further to this if a custom error ends with the words 'not found' a 404 response code is returned, otherwise a standard 400 response is returned. See the account service for some examples of custom errors thrown by the api, errors are caught in the accounts controller for each route and passed to next(err) which passes them to this global error handler.

module.exports = errorHandler;

function errorHandler(err, req, res, next) {
    switch (true) {
        case typeof err === 'string':
            // custom application error
            const is404 = err.toLowerCase().endsWith('not found');
            const statusCode = is404 ? 404 : 400;
            return res.status(statusCode).json({ message: err });
        case err.name === 'UnauthorizedError':
            // jwt authentication error
            return res.status(401).json({ message: 'Unauthorized' });
        default:
            return res.status(500).json({ message: err.message });
    }
}
 

Validate Request Middleware

Path: /_middleware/validate-request.js

The validate request middleware function validates the body of a request against a Joi schema object.

It used by schema middleware functions in controllers to validate the request against the schema for a specific route (e.g. authenticateSchema in the accounts controller).

module.exports = validateRequest;

function validateRequest(req, next, schema) {
    const options = {
        abortEarly: false, // include all errors
        allowUnknown: true, // ignore unknown props
        stripUnknown: true // remove unknown props
    };
    const { error, value } = schema.validate(req.body, options);
    if (error) {
        next(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
    } else {
        req.body = value;
        next();
    }
}
 

Accounts Feature Folder

Path: /accounts

The accounts folder contains all code that is specific to the accounts feature of the node.js + mysql boilerplate api.

 

Sequelize Account Model

Path: /accounts/account.model.js

The account model uses Sequelize to define the schema for the accounts table in the MySQL database. The exported Sequelize model object gives full access to perform CRUD (create, read, update, delete) operations on accounts in MySQL, see the account service below for examples of it being used (via the db helper).

Fields with the type DataTypes.VIRTUAL are sequelize virtual fields that are not persisted in the database, they are convenience properties on the model that can include multiple field values (e.g. isVerified).

The defaultScope configures the model to exclude the password hash from query results by default. The withHash scope can be used to query accounts and include the password hash field in results.

The one-to-many relationship between accounts and refresh tokens is defined in the database wrapper.

const { DataTypes } = require('sequelize');

module.exports = model;

function model(sequelize) {
    const attributes = {
        email: { type: DataTypes.STRING, allowNull: false },
        passwordHash: { type: DataTypes.STRING, allowNull: false },
        title: { type: DataTypes.STRING, allowNull: false },
        firstName: { type: DataTypes.STRING, allowNull: false },
        lastName: { type: DataTypes.STRING, allowNull: false },
        acceptTerms: { type: DataTypes.BOOLEAN },
        role: { type: DataTypes.STRING, allowNull: false },
        verificationToken: { type: DataTypes.STRING },
        verified: { type: DataTypes.DATE },
        resetToken: { type: DataTypes.STRING },
        resetTokenExpires: { type: DataTypes.DATE },
        passwordReset: { type: DataTypes.DATE },
        created: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
        updated: { type: DataTypes.DATE },
        isVerified: {
            type: DataTypes.VIRTUAL,
            get() { return !!(this.verified || this.passwordReset); }
        }
    };

    const options = {
        // disable default timestamp fields (createdAt and updatedAt)
        timestamps: false, 
        defaultScope: {
            // exclude password hash by default
            attributes: { exclude: ['passwordHash'] }
        },
        scopes: {
            // include hash with this scope
            withHash: { attributes: {}, }
        }        
    };

    return sequelize.define('account', attributes, options);
}
 

Sequelize Refresh Token Model

Path: /accounts/refresh-token.model.js

The refresh token model uses Sequelize to define the schema for the refreshTokens table in the MySQL database. The exported Sequelize model object gives full access to perform CRUD (create, read, update, delete) operations on refresh tokens in MySQL, see the account service below for examples of it being used (via the db helper).

The DataTypes.VIRTUAL properties are convenience properties available on the sequelize model that don't get persisted to the MySQL database.

The one-to-many relationship between accounts and refresh tokens is defined in the database wrapper.

const { DataTypes } = require('sequelize');

module.exports = model;

function model(sequelize) {
    const attributes = {
        token: { type: DataTypes.STRING },
        expires: { type: DataTypes.DATE },
        created: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
        createdByIp: { type: DataTypes.STRING },
        revoked: { type: DataTypes.DATE },
        revokedByIp: { type: DataTypes.STRING },
        replacedByToken: { type: DataTypes.STRING },
        isExpired: {
            type: DataTypes.VIRTUAL,
            get() { return Date.now() >= this.expires; }
        },
        isActive: {
            type: DataTypes.VIRTUAL,
            get() { return !this.revoked && !this.isExpired; }
        }
    };

    const options = {
        // disable default timestamp fields (createdAt and updatedAt)
        timestamps: false
    };

    return sequelize.define('refreshToken', attributes, options);
}
 

Account Service

Path: /accounts/account.service.js

The account service contains the core business logic for account sign up & verification, authentication with JWT & refresh tokens, forgot password & reset password functionality, as well as CRUD methods for managing account data. The service encapsulates all interaction with the sequelize account models and exposes a simple set of methods which are used by the accounts controller.

The top of the file contains the exported service object with just the method names to make it easy to see all the methods at a glance, the rest of the file contains the implementation functions for each service method, followed by local helper functions.

const config = require('config.json');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const crypto = require("crypto");
const { Op } = require('sequelize');
const sendEmail = require('_helpers/send-email');
const db = require('_helpers/db');
const Role = require('_helpers/role');

module.exports = {
    authenticate,
    refreshToken,
    revokeToken,
    register,
    verifyEmail,
    forgotPassword,
    validateResetToken,
    resetPassword,
    getAll,
    getById,
    create,
    update,
    delete: _delete
};

async function authenticate({ email, password, ipAddress }) {
    const account = await db.Account.scope('withHash').findOne({ where: { email } });

    if (!account || !account.isVerified || !(await bcrypt.compare(password, account.passwordHash))) {
        throw 'Email or password is incorrect';
    }

    // authentication successful so generate jwt and refresh tokens
    const jwtToken = generateJwtToken(account);
    const refreshToken = generateRefreshToken(account, ipAddress);

    // save refresh token
    await refreshToken.save();

    // return basic details and tokens
    return {
        ...basicDetails(account),
        jwtToken,
        refreshToken: refreshToken.token
    };
}

async function refreshToken({ token, ipAddress }) {
    const refreshToken = await getRefreshToken(token);
    const account = await refreshToken.getAccount();

    // replace old refresh token with a new one and save
    const newRefreshToken = generateRefreshToken(account, ipAddress);
    refreshToken.revoked = Date.now();
    refreshToken.revokedByIp = ipAddress;
    refreshToken.replacedByToken = newRefreshToken.token;
    await refreshToken.save();
    await newRefreshToken.save();

    // generate new jwt
    const jwtToken = generateJwtToken(account);

    // return basic details and tokens
    return {
        ...basicDetails(account),
        jwtToken,
        refreshToken: newRefreshToken.token
    };
}

async function revokeToken({ token, ipAddress }) {
    const refreshToken = await getRefreshToken(token);

    // revoke token and save
    refreshToken.revoked = Date.now();
    refreshToken.revokedByIp = ipAddress;
    await refreshToken.save();
}

async function register(params, origin) {
    // validate
    if (await db.Account.findOne({ where: { email: params.email } })) {
        // send already registered error in email to prevent account enumeration
        return await sendAlreadyRegisteredEmail(params.email, origin);
    }

    // create account object
    const account = new db.Account(params);

    // first registered account is an admin
    const isFirstAccount = (await db.Account.count()) === 0;
    account.role = isFirstAccount ? Role.Admin : Role.User;
    account.verificationToken = randomTokenString();

    // hash password
    account.passwordHash = await hash(params.password);

    // save account
    await account.save();

    // send email
    await sendVerificationEmail(account, origin);
}

async function verifyEmail({ token }) {
    const account = await db.Account.findOne({ where: { verificationToken: token } });

    if (!account) throw 'Verification failed';

    account.verified = Date.now();
    account.verificationToken = null;
    await account.save();
}

async function forgotPassword({ email }, origin) {
    const account = await db.Account.findOne({ where: { email } });

    // always return ok response to prevent email enumeration
    if (!account) return;

    // create reset token that expires after 24 hours
    account.resetToken = randomTokenString();
    account.resetTokenExpires = new Date(Date.now() + 24*60*60*1000);
    await account.save();

    // send email
    await sendPasswordResetEmail(account, origin);
}

async function validateResetToken({ token }) {
    const account = await db.Account.findOne({
        where: {
            resetToken: token,
            resetTokenExpires: { [Op.gt]: Date.now() }
        }
    });

    if (!account) throw 'Invalid token';

    return account;
}

async function resetPassword({ token, password }) {
    const account = await validateResetToken({ token });

    // update password and remove reset token
    account.passwordHash = await hash(password);
    account.passwordReset = Date.now();
    account.resetToken = null;
    await account.save();
}

async function getAll() {
    const accounts = await db.Account.findAll();
    return accounts.map(x => basicDetails(x));
}

async function getById(id) {
    const account = await getAccount(id);
    return basicDetails(account);
}

async function create(params) {
    // validate
    if (await db.Account.findOne({ where: { email: params.email } })) {
        throw 'Email "' + params.email + '" is already registered';
    }

    const account = new db.Account(params);
    account.verified = Date.now();

    // hash password
    account.passwordHash = await hash(params.password);

    // save account
    await account.save();

    return basicDetails(account);
}

async function update(id, params) {
    const account = await getAccount(id);

    // validate (if email was changed)
    if (params.email && account.email !== params.email && await db.Account.findOne({ where: { email: params.email } })) {
        throw 'Email "' + params.email + '" is already taken';
    }

    // hash password if it was entered
    if (params.password) {
        params.passwordHash = await hash(params.password);
    }

    // copy params to account and save
    Object.assign(account, params);
    account.updated = Date.now();
    await account.save();

    return basicDetails(account);
}

async function _delete(id) {
    const account = await getAccount(id);
    await account.destroy();
}

// helper functions

async function getAccount(id) {
    const account = await db.Account.findByPk(id);
    if (!account) throw 'Account not found';
    return account;
}

async function getRefreshToken(token) {
    const refreshToken = await db.RefreshToken.findOne({ where: { token } });
    if (!refreshToken || !refreshToken.isActive) throw 'Invalid token';
    return refreshToken;
}

async function hash(password) {
    return await bcrypt.hash(password, 10);
}

function generateJwtToken(account) {
    // create a jwt token containing the account id that expires in 15 minutes
    return jwt.sign({ sub: account.id, id: account.id }, config.secret, { expiresIn: '15m' });
}

function generateRefreshToken(account, ipAddress) {
    // create a refresh token that expires in 7 days
    return new db.RefreshToken({
        accountId: account.id,
        token: randomTokenString(),
        expires: new Date(Date.now() + 7*24*60*60*1000),
        createdByIp: ipAddress
    });
}

function randomTokenString() {
    return crypto.randomBytes(40).toString('hex');
}

function basicDetails(account) {
    const { id, title, firstName, lastName, email, role, created, updated, isVerified } = account;
    return { id, title, firstName, lastName, email, role, created, updated, isVerified };
}

async function sendVerificationEmail(account, origin) {
    let message;
    if (origin) {
        const verifyUrl = `${origin}/account/verify-email?token=${account.verificationToken}`;
        message = `<p>Please click the below link to verify your email address:</p>
                   <p><a href="${verifyUrl}">${verifyUrl}</a></p>`;
    } else {
        message = `<p>Please use the below token to verify your email address with the <code>/account/verify-email</code> api route:</p>
                   <p><code>${account.verificationToken}</code></p>`;
    }

    await sendEmail({
        to: account.email,
        subject: 'Sign-up Verification API - Verify Email',
        html: `<h4>Verify Email</h4>
               <p>Thanks for registering!</p>
               ${message}`
    });
}

async function sendAlreadyRegisteredEmail(email, origin) {
    let message;
    if (origin) {
        message = `<p>If you don't know your password please visit the <a href="${origin}/account/forgot-password">forgot password</a> page.</p>`;
    } else {
        message = `<p>If you don't know your password you can reset it via the <code>/account/forgot-password</code> api route.</p>`;
    }

    await sendEmail({
        to: email,
        subject: 'Sign-up Verification API - Email Already Registered',
        html: `<h4>Email Already Registered</h4>
               <p>Your email <strong>${email}</strong> is already registered.</p>
               ${message}`
    });
}

async function sendPasswordResetEmail(account, origin) {
    let message;
    if (origin) {
        const resetUrl = `${origin}/account/reset-password?token=${account.resetToken}`;
        message = `<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
                   <p><a href="${resetUrl}">${resetUrl}</a></p>`;
    } else {
        message = `<p>Please use the below token to reset your password with the <code>/account/reset-password</code> api route:</p>
                   <p><code>${account.resetToken}</code></p>`;
    }

    await sendEmail({
        to: account.email,
        subject: 'Sign-up Verification API - Reset Password',
        html: `<h4>Reset Password Email</h4>
               ${message}`
    });
}
 

Express.js Accounts Controller

Path: /accounts/accounts.controller.js

The accounts controller defines all /accounts routes for the Node.js + MySQL boilerplate api, the route definitions are grouped together at the top of the file and the implementation functions are below, followed by local helper functions. The controller is bound to the /accounts path in the main server.js file.

Routes that require authorization include the middleware function authorize() and optionally specify a role (e.g. authorize(Role.Admin), if a role is specified then the route is restricted to users in that role, otherwise the route is restricted to all authenticated users regardless of role. The auth logic is located in the authorize middleware.

The route functions revokeToken, getById, update and _delete include an extra custom authorization check to prevent non-admin users from accessing accounts other than their own. So regular user accounts (Role.User) have CRUD access to their own account but not to others, and admin accounts (Role.Admin) have full CRUD access to all accounts.

Routes that require schema validation include a middleware function with the naming convention <route>Schema (e.g. authenticateSchema). Each schema validation function defines a schema for the request body using the Joi library and calls validateRequest(req, next, schema) to ensure the request body is valid. If validation succeeds the request continues to the next middleware function (the route function), otherwise an error is returned with details of why validation failed. For more info about Joi schema validation see https://www.npmjs.com/package/joi.

Express is the web server used by the boilerplate api, it's one of the most popular web application frameworks for Node.js. For more info see https://expressjs.com/.

const express = require('express');
const router = express.Router();
const Joi = require('joi');
const validateRequest = require('_middleware/validate-request');
const authorize = require('_middleware/authorize')
const Role = require('_helpers/role');
const accountService = require('./account.service');

// routes
router.post('/authenticate', authenticateSchema, authenticate);
router.post('/refresh-token', refreshToken);
router.post('/revoke-token', authorize(), revokeTokenSchema, revokeToken);
router.post('/register', registerSchema, register);
router.post('/verify-email', verifyEmailSchema, verifyEmail);
router.post('/forgot-password', forgotPasswordSchema, forgotPassword);
router.post('/validate-reset-token', validateResetTokenSchema, validateResetToken);
router.post('/reset-password', resetPasswordSchema, resetPassword);
router.get('/', authorize(Role.Admin), getAll);
router.get('/:id', authorize(), getById);
router.post('/', authorize(Role.Admin), createSchema, create);
router.put('/:id', authorize(), updateSchema, update);
router.delete('/:id', authorize(), _delete);

module.exports = router;

function authenticateSchema(req, res, next) {
    const schema = Joi.object({
        email: Joi.string().required(),
        password: Joi.string().required()
    });
    validateRequest(req, next, schema);
}

function authenticate(req, res, next) {
    const { email, password } = req.body;
    const ipAddress = req.ip;
    accountService.authenticate({ email, password, ipAddress })
        .then(({ refreshToken, ...account }) => {
            setTokenCookie(res, refreshToken);
            res.json(account);
        })
        .catch(next);
}

function refreshToken(req, res, next) {
    const token = req.cookies.refreshToken;
    const ipAddress = req.ip;
    accountService.refreshToken({ token, ipAddress })
        .then(({ refreshToken, ...account }) => {
            setTokenCookie(res, refreshToken);
            res.json(account);
        })
        .catch(next);
}

function revokeTokenSchema(req, res, next) {
    const schema = Joi.object({
        token: Joi.string().empty('')
    });
    validateRequest(req, next, schema);
}

function revokeToken(req, res, next) {
    // accept token from request body or cookie
    const token = req.body.token || req.cookies.refreshToken;
    const ipAddress = req.ip;

    if (!token) return res.status(400).json({ message: 'Token is required' });

    // users can revoke their own tokens and admins can revoke any tokens
    if (!req.user.ownsToken(token) && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    accountService.revokeToken({ token, ipAddress })
        .then(() => res.json({ message: 'Token revoked' }))
        .catch(next);
}

function registerSchema(req, res, next) {
    const schema = Joi.object({
        title: Joi.string().required(),
        firstName: Joi.string().required(),
        lastName: Joi.string().required(),
        email: Joi.string().email().required(),
        password: Joi.string().min(6).required(),
        confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
        acceptTerms: Joi.boolean().valid(true).required()
    });
    validateRequest(req, next, schema);
}

function register(req, res, next) {
    accountService.register(req.body, req.get('origin'))
        .then(() => res.json({ message: 'Registration successful, please check your email for verification instructions' }))
        .catch(next);
}

function verifyEmailSchema(req, res, next) {
    const schema = Joi.object({
        token: Joi.string().required()
    });
    validateRequest(req, next, schema);
}

function verifyEmail(req, res, next) {
    accountService.verifyEmail(req.body)
        .then(() => res.json({ message: 'Verification successful, you can now login' }))
        .catch(next);
}

function forgotPasswordSchema(req, res, next) {
    const schema = Joi.object({
        email: Joi.string().email().required()
    });
    validateRequest(req, next, schema);
}

function forgotPassword(req, res, next) {
    accountService.forgotPassword(req.body, req.get('origin'))
        .then(() => res.json({ message: 'Please check your email for password reset instructions' }))
        .catch(next);
}

function validateResetTokenSchema(req, res, next) {
    const schema = Joi.object({
        token: Joi.string().required()
    });
    validateRequest(req, next, schema);
}

function validateResetToken(req, res, next) {
    accountService.validateResetToken(req.body)
        .then(() => res.json({ message: 'Token is valid' }))
        .catch(next);
}

function resetPasswordSchema(req, res, next) {
    const schema = Joi.object({
        token: Joi.string().required(),
        password: Joi.string().min(6).required(),
        confirmPassword: Joi.string().valid(Joi.ref('password')).required()
    });
    validateRequest(req, next, schema);
}

function resetPassword(req, res, next) {
    accountService.resetPassword(req.body)
        .then(() => res.json({ message: 'Password reset successful, you can now login' }))
        .catch(next);
}

function getAll(req, res, next) {
    accountService.getAll()
        .then(accounts => res.json(accounts))
        .catch(next);
}

function getById(req, res, next) {
    // users can get their own account and admins can get any account
    if (Number(req.params.id) !== req.user.id && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    accountService.getById(req.params.id)
        .then(account => account ? res.json(account) : res.sendStatus(404))
        .catch(next);
}

function createSchema(req, res, next) {
    const schema = Joi.object({
        title: Joi.string().required(),
        firstName: Joi.string().required(),
        lastName: Joi.string().required(),
        email: Joi.string().email().required(),
        password: Joi.string().min(6).required(),
        confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
        role: Joi.string().valid(Role.Admin, Role.User).required()
    });
    validateRequest(req, next, schema);
}

function create(req, res, next) {
    accountService.create(req.body)
        .then(account => res.json(account))
        .catch(next);
}

function updateSchema(req, res, next) {
    const schemaRules = {
        title: Joi.string().empty(''),
        firstName: Joi.string().empty(''),
        lastName: Joi.string().empty(''),
        email: Joi.string().email().empty(''),
        password: Joi.string().min(6).empty(''),
        confirmPassword: Joi.string().valid(Joi.ref('password')).empty('')
    };

    // only admins can update role
    if (req.user.role === Role.Admin) {
        schemaRules.role = Joi.string().valid(Role.Admin, Role.User).empty('');
    }

    const schema = Joi.object(schemaRules).with('password', 'confirmPassword');
    validateRequest(req, next, schema);
}

function update(req, res, next) {
    // users can update their own account and admins can update any account
    if (Number(req.params.id) !== req.user.id && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    accountService.update(req.params.id, req.body)
        .then(account => res.json(account))
        .catch(next);
}

function _delete(req, res, next) {
    // users can delete their own account and admins can delete any account
    if (Number(req.params.id) !== req.user.id && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    accountService.delete(req.params.id)
        .then(() => res.json({ message: 'Account deleted successfully' }))
        .catch(next);
}

// helper functions

function setTokenCookie(res, token) {
    // create cookie with refresh token that expires in 7 days
    const cookieOptions = {
        httpOnly: true,
        expires: new Date(Date.now() + 7*24*60*60*1000)
    };
    res.cookie('refreshToken', token, cookieOptions);
}
 

Api Config

Path: /config.json

The api config file contains configuration data for the boilerplate api, it includes the database connection options for the MySQL database, the secret used for signing and verifying JWT tokens, the emailFrom address used to send emails, and the smtpOptions used to connect and authenticate with an email server.

Configure SMTP settings for email within the smtpOptions property. For testing you can create a free account in one click at https://ethereal.email/ and copy the options below the title Nodemailer configuration.

IMPORTANT: The secret property is used to sign and verify JWT tokens for authentication, change it with your own random string to ensure nobody else can generate a JWT with the same secret to gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).

{
    "database": {
        "host": "localhost",
        "port": 3306,
        "user": "root",
        "password": "",
        "database": "node-mysql-signup-verification-api"
    },
    "secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING",
    "emailFrom": "[email protected]",
    "smtpOptions": {
        "host": "[ENTER YOUR OWN SMTP OPTIONS OR CREATE FREE TEST ACCOUNT IN ONE CLICK AT https://ethereal.email/]",
        "port": 587,
        "auth": {
            "user": "",
            "pass": ""
        }
    }
}
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies which get installed when you run npm install.

The scripts section contains scripts that are executed by running the command npm run <script name>, the start script can also be run with the shortcut command npm start.

The start script starts the boilerplate api normally using node, and the start:dev script starts the api in development mode using nodemon which automatically restarts the server when a file is changed.

For more info see https://docs.npmjs.com/files/package.json.

{
    "name": "node-mysql-signup-verification-api",
    "version": "1.0.0",
    "description": "NodeJS + MySQL API for Email Sign Up with Verification, Authentication & Forgot Password",
    "license": "MIT",
    "repository": {
        "type": "git",
        "url": "https://github.com/cornflourblue/node-mysql-signup-verification-api.git"
    },
    "scripts": {
        "start": "node ./server.js",
        "start:dev": "nodemon ./server.js"
    },
    "dependencies": {
        "bcryptjs": "^2.4.3",
        "body-parser": "^1.19.0",
        "cookie-parser": "^1.4.5",
        "cors": "^2.8.5",
        "express-jwt": "^6.0.0",
        "express": "^4.17.1",
        "joi": "^17.2.1",
        "jsonwebtoken": "^8.5.1",
        "mysql2": "^2.1.0",
        "nodemailer": "^6.4.11",
        "rootpath": "^0.1.2",
        "sequelize": "^6.3.4",
        "swagger-ui-express": "^4.1.4",
        "yamljs": "^0.3.0"
    },
    "devDependencies": {
        "nodemon": "^2.0.3"
    }
}
 

Server Startup File

Path: /server.js

The server.js file is the entry point into the boilerplate Node.js api, it configures application middleware, binds controllers to routes and starts the Express web server for the api.

require('rootpath')();
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const errorHandler = require('_middleware/error-handler');

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cookieParser());

// allow cors requests from any origin and with credentials
app.use(cors({ origin: (origin, callback) => callback(null, true), credentials: true }));

// api routes
app.use('/accounts', require('./accounts/accounts.controller'));

// swagger docs route
app.use('/api-docs', require('_helpers/swagger'));

// global error handler
app.use(errorHandler);

// start server
const port = process.env.NODE_ENV === 'production' ? (process.env.PORT || 80) : 4000;
app.listen(port, () => console.log('Server listening on port ' + port));
 

Swagger API Documentation

Path: /swagger.yaml

The Swagger YAML file describes the entire Node.js Boilerplate API using the OpenAPI Specification format, it includes descriptions of all routes and HTTP methods on each route, request and response schemas, path parameters, and authentication methods.

The YAML documentation is used by the swagger.js helper to automatically generate and serve interactive Swagger UI documentation on the /api-docs route of the boilerplate api. To preview the Swagger UI documentation without running the api simply copy and paste the below YAML into the swagger editor at https://editor.swagger.io/.

openapi: 3.0.0
info:
  title: Node.js Sign-up and Verification API
  description: Node.js + MySQL - API with email sign-up, verification, authentication and forgot password
  version: 1.0.0

servers:
  - url: http://localhost:4000
    description: Local development server

paths:
  /accounts/authenticate:
    post:
      summary: Authenticate account credentials and return a JWT token and a cookie with a refresh token
      description: Accounts must be verified before authenticating.
      operationId: authenticate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  example: "[email protected]"
                password:
                  type: string
                  example: "pass123"
              required:
                - email
                - password
      responses:
        "200":
          description: Account details, a JWT access token and a refresh token cookie
          headers:
            Set-Cookie:
              description: "`refreshToken`"
              schema:
                type: string
                example: refreshToken=51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550; Path=/; Expires=Tue, 16 Jun 2020 09:14:17 GMT; HttpOnly
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  title:
                    type: string
                    example: "Mr"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  email:
                    type: string
                    example: "[email protected]"
                  role:
                    type: string
                    example: "Admin"
                  created:
                    type: string
                    example: "2020-05-05T09:12:57.848Z"
                  isVerified:
                    type: boolean
                    example: true
                  jwtToken:
                    type: string
                    example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWIxMmUxOTdlMDZhNzZjY2RlZmMxMjEiLCJpZCI6IjVlYjEyZTE5N2UwNmE3NmNjZGVmYzEyMSIsImlhdCI6MTU4ODc1ODE1N30.xR9H0STbFOpSkuGA9jHNZOJ6eS7umHHqKRhI807YT1Y"
        "400":
          description: The email or password is incorrect
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Email or password is incorrect"
  /accounts/refresh-token:
    post:
      summary: Use a refresh token to generate a new JWT token and a new refresh token
      description: The refresh token is sent and returned via cookies.
      operationId: refreshToken
      parameters:
        - in: cookie
          name: refreshToken
          description: The `refreshToken` cookie
          schema:
            type: string
            example: 51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550
      responses:
        "200":
          description: Account details, a JWT access token and a new refresh token cookie
          headers:
            Set-Cookie:
              description: "`refreshToken`"
              schema:
                type: string
                example: refreshToken=51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550; Path=/; Expires=Tue, 16 Jun 2020 09:14:17 GMT; HttpOnly
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  title:
                    type: string
                    example: "Mr"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  email:
                    type: string
                    example: "[email protected]"
                  role:
                    type: string
                    example: "Admin"
                  created:
                    type: string
                    example: "2020-05-05T09:12:57.848Z"
                  isVerified:
                    type: boolean
                    example: true
                  jwtToken:
                    type: string
                    example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWIxMmUxOTdlMDZhNzZjY2RlZmMxMjEiLCJpZCI6IjVlYjEyZTE5N2UwNmE3NmNjZGVmYzEyMSIsImlhdCI6MTU4ODc1ODE1N30.xR9H0STbFOpSkuGA9jHNZOJ6eS7umHHqKRhI807YT1Y"
        "400":
          description: The refresh token is invalid, revoked or expired
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invalid token"
  /accounts/revoke-token:
    post:
      summary: Revoke a refresh token
      description: Admin users can revoke the tokens of any account, regular users can only revoke their own tokens.
      operationId: revokeToken
      security:
        - bearerAuth: []
      parameters:
        - in: cookie
          name: refreshToken
          description: The refresh token can be sent in a cookie or the post body, if both are sent the token in the body is used.
          schema:
            type: string
            example: 51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  example: "51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550"
      responses:
        "200":
          description: The refresh token was successfully revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Token revoked"
        "400":
          description: The refresh token is invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invalid token"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
  /accounts/register:
    post:
      summary: Register a new user account and send a verification email
      description: The first account registered in the system is assigned the `Admin` role, other accounts are assigned the `User` role.
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: "Mr"
                firstName:
                  type: string
                  example: "Jason"
                lastName:
                  type: string
                  example: "Watmore"
                email:
                  type: string
                  example: "[email protected]"
                password:
                  type: string
                  example: "pass123"
                confirmPassword:
                  type: string
                  example: "pass123"
                acceptTerms:
                  type: boolean
              required:
                - title
                - firstName
                - lastName
                - email
                - password
                - confirmPassword
                - acceptTerms
      responses:
        "200":
          description: The registration request was successful and a verification email has been sent to the specified email address
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Registration successful, please check your email for verification instructions"
  /accounts/verify-email:
    post:
      summary: Verify a new account with a verification token received by email after registration
      operationId: verifyEmail
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  example: "3c7f8d9c4cb348ff95a0b74a1452aa24fc9611bb76768bb9eafeeb826ddae2935f1880bc7713318f"
              required:
                - token
      responses:
        "200":
          description: Verification was successful so you can now login to the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Verification successful, you can now login"
        "400":
          description: Verification failed due to an invalid token
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Verification failed"
  /accounts/forgot-password:
    post:
      summary: Submit email address to reset the password on an account
      operationId: forgotPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  example: "[email protected]"
              required:
                - email
      responses:
        "200":
          description: The request was received and an email has been sent to the specified address with password reset instructions (if the email address is associated with an account)
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Please check your email for password reset instructions"
  /accounts/validate-reset-token:
    post:
      summary: Validate the reset password token received by email after submitting to the /accounts/forgot-password route
      operationId: validateResetToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  example: "3c7f8d9c4cb348ff95a0b74a1452aa24fc9611bb76768bb9eafeeb826ddae2935f1880bc7713318f"
              required:
                - token
      responses:
        "200":
          description: Token is valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Token is valid"
        "400":
          description: Token is invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invalid token"
  /accounts/reset-password:
    post:
      summary: Reset the password for an account
      operationId: resetPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  example: "3c7f8d9c4cb348ff95a0b74a1452aa24fc9611bb76768bb9eafeeb826ddae2935f1880bc7713318f"
                password:
                  type: string
                  example: "newPass123"
                confirmPassword:
                  type: string
                  example: "newPass123"
              required:
                - token
                - password
                - confirmPassword
      responses:
        "200":
          description: Password reset was successful so you can now login to the account with the new password
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Password reset successful, you can now login"
        "400":
          description: Password reset failed due to an invalid token
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invalid token"
  /accounts:
    get:
      summary: Get a list of all accounts
      description: Restricted to admin users.
      operationId: getAllAccounts
      security:
        - bearerAuth: []
      responses:
        "200":
          description: An array of all accounts
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                      example: "5eb12e197e06a76ccdefc121"
                    title:
                      type: string
                      example: "Mr"
                    firstName:
                      type: string
                      example: "Jason"
                    lastName:
                      type: string
                      example: "Watmore"
                    email:
                      type: string
                      example: "[email protected]"
                    role:
                      type: string
                      example: "Admin"
                    created:
                      type: string
                      example: "2020-05-05T09:12:57.848Z"
                    updated:
                      type: string
                      example: "2020-05-08T03:11:21.553Z"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
    post:
      summary: Create a new account
      description: Restricted to admin users.
      operationId: createAccount
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: "Mr"
                firstName:
                  type: string
                  example: "Jason"
                lastName:
                  type: string
                  example: "Watmore"
                email:
                  type: string
                  example: "[email protected]"
                password:
                  type: string
                  example: "pass123"
                confirmPassword:
                  type: string
                  example: "pass123"
                role:
                  type: string
                  enum: [Admin, User]
              required:
                - title
                - firstName
                - lastName
                - email
                - password
                - confirmPassword
                - role
      responses:
        "200":
          description: Account created successfully, verification is not required for accounts created with this endpoint. The details of the new account are returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  title:
                    type: string
                    example: "Mr"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  email:
                    type: string
                    example: "[email protected]"
                  role:
                    type: string
                    example: "Admin"
                  created:
                    type: string
                    example: "2020-05-05T09:12:57.848Z"
        "400":
          description: Email is already registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Email '[email protected]' is already registered"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
  /accounts/{id}:
    parameters:
      - in: path
        name: id
        description: Account id
        required: true
        example: "5eb12e197e06a76ccdefc121"
        schema:
          type: string
    get:
      summary: Get a single account by id
      description: Admin users can access any account, regular users are restricted to their own account.
      operationId: getAccountById
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Details of the specified account
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  title:
                    type: string
                    example: "Mr"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  email:
                    type: string
                    example: "[email protected]"
                  role:
                    type: string
                    example: "Admin"
                  created:
                    type: string
                    example: "2020-05-05T09:12:57.848Z"
                  updated:
                    type: string
                    example: "2020-05-08T03:11:21.553Z"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
    put:
      summary: Update an account
      description: Admin users can update any account including role, regular users are restricted to their own account and cannot update role.
      operationId: updateAccount
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: "Mr"
                firstName:
                  type: string
                  example: "Jason"
                lastName:
                  type: string
                  example: "Watmore"
                email:
                  type: string
                  example: "[email protected]"
                password:
                  type: string
                  example: "pass123"
                confirmPassword:
                  type: string
                  example: "pass123"
                role:
                  type: string
                  enum: [Admin, User]
      responses:
        "200":
          description: Account updated successfully. The details of the updated account are returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  title:
                    type: string
                    example: "Mr"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  email:
                    type: string
                    example: "[email protected]"
                  role:
                    type: string
                    example: "Admin"
                  created:
                    type: string
                    example: "2020-05-05T09:12:57.848Z"
                  updated:
                    type: string
                    example: "2020-05-08T03:11:21.553Z"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
    delete:
      summary: Delete an account
      description: Admin users can delete any account, regular users are restricted to their own account.
      operationId: deleteAccount
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Account deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Account deleted successfully"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  responses:
    UnauthorizedError:
      description: Access token is missing or invalid, or the user does not have access to perform the action
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: "Unauthorized"
    NotFoundError:
      description: Not Found
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: "Not Found"

 


Need Some NodeJS Help?

Search fiverr for freelance NodeJS developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by