Node.js + MySQL - Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password
Tutorial built with Node.js, Sequelize and MySQL
Other versions available:
- Node: Node.js + MongoDB
- .NET: .NET 6.0, 5.0, ASP.NET Core 3.1
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
- Run the boilerplate API locally
- Run with a React App
- Run with an Angular App
- Test the API with Postman
- Boilerplate project structure and code documentation
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
- Install NodeJS and NPM from https://nodejs.org/en/download/.
- 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.
- Download or clone the project source code from https://github.com/cornflourblue/node-mysql-signup-verification-api
- Install all required npm packages by running
npm install
ornpm i
from the command line in the project root folder (where the package.json is located). - 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. - Start the api by running
npm start
(ornpm run start:dev
to start with nodemon) from the command line in the project root folder, you should see the messageServer listening on port 4000
, and you can view the Swagger API documentation athttp://localhost:4000/api-docs
. - 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.
- Download or clone the React tutorial code from https://github.com/cornflourblue/react-signup-verification-boilerplate
- Install all required npm packages by running
npm install
ornpm i
from the command line in the project root folder (where the package.json is located). - Remove or comment out the 2 lines below the comment
// setup fake backend
located in the/src/index.jsx
file. - 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.
- Download or clone the Angular 10 tutorial code from https://github.com/cornflourblue/angular-10-signup-verification-boilerplate
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Remove or comment out the line below the comment
// provider used to create fake backend
located in the/src/app/app.module.ts
file. - 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:
- Register a new account
- Verify an account
- Access an account with a forgotten password
- Reset the password of an account
- Authenticate to get a JWT token and a refresh token
- Get a list of all accounts
- Update an account
- Use a refresh token to get a new JWT token
- Revoke a refresh token
- Delete an account
How to register a new account with Postman
To register a new account with the Node.js boilerplate api follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the register route of your local API -
http://localhost:4000/accounts/register
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- 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 }
- 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:
How to verify an account with Postman
To verify an account with the Node api follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/accounts/verify-email
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- 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" }
- 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:
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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/accounts/forgot-password
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- Enter a JSON object containing the email of the account with the forgotten password in the "Body" textarea, e.g:
{ "email": "[email protected]" }
- 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:
How to reset the password of an account with Postman
To reset the password of an account with the api follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/accounts/reset-password
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- 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" }
- 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:
How to authenticate with Postman
To authenticate an account with the api and get a JWT token follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/accounts/authenticate
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- Enter a JSON object containing the account email and password in the "Body" textarea:
{ "email": "[email protected]", "password": "my-super-secret-password" }
- 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.
- 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:
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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the users route of your local API -
http://localhost:4000/accounts
- 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.
- 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:
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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "PUT" with the dropdown selector on the left of the URL input field.
- 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
- 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.
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- 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" }
- 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:
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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the refresh token route of your local API -
http://localhost:4000/accounts/refresh-token
- 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.
- 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:
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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/accounts/revoke-token
- 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.
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- 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" }
- 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:
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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "DELETE" with the dropdown selector on the left of the URL input field.
- 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
- 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.
- 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:
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
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
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
andRefreshToken
models and attaches them to the exporteddb
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
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
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
)
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
The middleware folder contains Express.js middleware functions that can be used by different routes / features within the Node.js boilerplate api.
Authorize Middleware
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 auser
object is attached to thereq
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 therole
property and theownsToken
method to thereq.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
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
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
The accounts folder contains all code that is specific to the accounts feature of the node.js + mysql boilerplate api.
Sequelize Account Model
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
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
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
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
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
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
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
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
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!