1.0.9 • Published 7 years ago

hapi-bells v1.0.9

Weekly downloads
1
License
MIT
Repository
github
Last release
7 years ago

hapi-bells

A Hapi API template with cross cutting concerns baked into the template

Introduction

Purpose

We here at APEEYE have been playing around with the hapi.js framework (https://hapijs.com/) for a while now, and absolutely love it. Kudoes to the hapi.js team We set out to create a Hapi template, with some basic features baked into the template, making use of various plugins provided by the community.

Disclaimer: This template was created by novice developers. We are not claiming this is the absolute best template setup there possibly could be, so feel free to constructively critique it and make suggestions on how to improve it.

Usage

Dependancies

Installing

With npm installed, run

$ npm install hapi-bells

Then, with pm2 installed, run

$ pm2 start startup.json

To access the API Documentation, go to

$ http://localhost:8082

What is included in the template?

Key features included into the template include

  • Interactive Documentation
  • Request and Response Sanitization
  • CORS Support
  • Performance Measurement
  • Policies
  • Proxy Filter
  • Monitoring
  • Security
  • Data Encryption
  • Local Storage
  • CRON Jobs
  • API Configuration
  • Genral Utilities
  • SDK Generation
  • Logging
  • Load Balancing

Key Template Folders and Files

  • api_server.js - This is the main Hapi server file. In this file you can configure your hapi server and change plugins
  • routes.js - This is the hapi server api routes file. In this file you can set up your API paths.
  • startup.json - This file is used by pm2 to start the project. In this file you can configure the number of instances of the API to spin up, specify log file locations and give the API a friendly name which will display when running

    $ pm2 monit
  • path_handlers folder - This folder contains the handlers for the API paths. One file is used per group of API paths.

  • db folder - This folder contains local storage files, used to store whatever is required by the API.
  • config folder - This folder contains the API configuration JSON file, which can be used to store any configurations the API required.
  • policies folder - This folder contains all the API policies which can be used to decorate the API path with.
  • public folder - This folder contains all the elements of the API that would be publically accessible, like the Swagger UI for example.
  • templates folder - This folder contains all the templates required by the API.

Template Features and Examples

Documentation

Plugins and Tools used

How it was implemented in the API Template

In the api_server.js javaScript file, the swaggerOptions object is used to configure Swagger. The tags in the swaggerOptions object specifies the top level path names of the API. For each path created, there should be an associated path handler javascript file placed in the path_handlers folder. This path handler file is then included in the routes.js file to access the handler functions. For example: The System path specified, would have an associated javascript file systemEndpoints.js located in the path_handlers folder, containing all the functions associated with the system paths.

'use strict';
const Hapi = require('hapi'); //REST API framework
const Inert = require('inert'); //handler methods for static files
const Vision = require('vision'); //decorates req and resp interfaces
const Blipp = require('blipp'); //display routes table on startup in console
const Boom = require('boom'); //http error status codes
const Routes = require('./routes'); //cors support

 
const  server = new Hapi.Server();
server.connection({
    host: (process.env.HOST || 'localhost'),
    port: (process.env.PORT || 8082),
    routes: { cors: true }
});

// setup swagger options
const swaggerOptions = {
    info: {
        version: '1',
        title: 'MASTER TEMPLATE',
        description: 'Master API Teplate with cross cutting concerns baked into the template.'
    },
    tags: [
			{
			'name': 'System',
			'description': 'Internal Operations',
			'externalDocs': {
				'description': 'Find out more',
				'url': 'http://www.apeeye.com'
				}
			}, 
			{
			'name': 'User',
			'description': 'Consumer Operations',
			'externalDocs': {
				'description': 'Find out more',
				'url': 'http://www.apeeye.com'
				}
			}, 
			{
			'name': 'TestCases',
			'description': 'Verification',
			'externalDocs': {
				'description': 'Find out more',
				'url': 'http://www.apeeye.com'
				}
			}
		]
};

// register plug-ins
server.register([
    Inert,
    Vision,
    Blipp,
    {
        register: require('hapi-swagger'),
        options: swaggerOptions
    },
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

 
// add routes
server.route(Routes);

// add templates support with handlebars
server.views({
    path: 'templates',
    engines: { html: require('handlebars') },
    partialsPath: './templates/withPartials',
    helpersPath: './templates/helpers',
    isCached: false
})

As a project may be a mixture of web pages and API endpoints you need to tag the routes you wish Swagger to document. Simply add the tags: ['api'] property to the route object for any endpoint you want document.

const systemEndpoints = require('./path_handlers/systemEndpoints.js'); //system path Handlers

module.exports = [
{
    method: 'GET',
    path: '/System/{id}/',
    config: {
        handler: systemEndpoints.testfunction,
        description: 'Get test functikon',
        notes: 'Returns a todo item by the id passed in the path',
        tags: ['api'], // ADD THIS TAG
        validate: {
            params: {
                id : Joi.number()
                        .required()
                        .description('the id for the todo item'),
            }
        }
    },
}];

Once you have run the API, a Swagger UI document is generated representing your API structure. A swagger.json object is constructed in the background, and would be accessible at: http://localhost:8082/swagger.json

You can use this swagger.json object to generate associated SDK documentation at http://editor.swagger.io/. Import the swagger.json file into the swagger editor, and select Generate Client => html2 This will download a index.html file, which you can copy to your public/docs folder.

Side note: This SDK documentation process could probably be automated by running an independant swagger codegen instance.

Request and Response Sanitization

Plugins and Tools used

  • Request query, payload, and params sanitization for Hapi - disinfect by genediazjr
  • Object schema validation - joi by hapijs
  • HTTP-friendly error objects - boom by hapijs
  • Reply to hapi requests with a statusCode and optional headers - hapi-status by daanvanham

How it was implemented in the API Template

The disinfect plugin gives the template the ability to implement custom sanitization and per-route configuration. Plugins used in the template, must first be registered in the api_server.js file.

// register plug-ins
server.register([
    Inert,
    Vision,
    Blipp,
	{
	register: require('disinfect'),
	  options: {
		    disinfectQuery: true,
		    disinfectParams: true,
		    disinfectPayload: true
			}
	},	
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

Once the plugin is registered, it is available to the API routes.

	{ /*/System/API_Ping/*/
		method: 'GET',
		path: '/System/API_Ping/',
		config: {
		   plugins: {
			disinfect: {
			  disinfectQuery: true,
			  disinfectParams: true,
			  disinfectPayload: true
				}	
			},
	handler:  systemEndpoints.API_Ping,
	description: 'API Heartbeat Monitoring',
	notes : 'Endpoint used for Heartbeat Monitoring. Monitoring will use this endpoint to check if the API is up and available.',
			tags: ['api']
		},
	}

CORS Support

Plugins and Tools used

How it was implemented in the API Template

CORS support has been implemented in the api_server.js file to enables CORS on all server responses, securely from all origins, with access-control-allow-credentials: true. The plugin hapi-cors-headers extends the hapi server to include CORS headers in all responses.

const corsHeaders = require('hapi-cors-headers');

...
 
//Adds cors support
server.ext('onPreResponse', corsHeaders);

Performance Measurements

Plugins and Tools used

How it was implemented in the API Template

The hapi-response-time plugin is registered in the api_server.js file.

server.register([
    Inert,
    Vision,
    Blipp,
	{
	  register: require('hapi-response-time')
	},
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

This plugin will add following headers to each request. The time represented is in the UNIX/Epoch time.

  • x-req-time: The time on which request is received on server
  • x-res-end: The time before sending the response
  • x-response-time: The difference between above two, i.e. the time taken by server to process the request before sending the response

Policies

Plugins and Tools used

  • Policies for hapi routes - mrhorse by mark-bradshaw

How it was implemented in the API Template

Policies are implemented using the mrhorse plugin. The plugin is registered in the api_server.js file. The plugin will load all policy javascipt files located in the policies folder. For example: If this policy file located in the policies folder is named isAdmin.js, then the policy would be identified as isAdmin and loaded on startup of the API server.

// register plug-ins
server.register([
    Inert,
    Vision,
    Blipp,
	{
	register: require('mrhorse'),
	options: {
		   policyDirectory: __dirname + '/policies'
		} 
	}, 	
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

Policies are just a simple javascript file that exports one javascript function.

var isAdmin = function(request, reply, next) {
   var role = _do_something_to_check_user_role(request);
   if (role && role === 'admin') {
       return next(null, true); // All is well with this request.  Proceed to the next policy or the route handler.
   } else {
       return next(null, false); // This policy is not satisfied.  Return a 403 forbidden.
   }
};

// This is optional.  It will default to 'onPreHandler' unless you use a different defaultApplyPoint.
isAdmin.applyPoint = 'onPreHandler';

module.exports = isAdmin;

The policy function must call the next callback and provide a boolean value indicating whether the request can continue on for further processing in the hapi lifecycle next(null, true). If you don't call the next callback, hapi will never respond to the request. It will timeout.

Policies are applied to the routes.js file, on the individual API endpoints.

server.route({
    method: 'GET',
    path: '/admin',
    handler: function(request, reply) {},
    config: {
        plugins: {
            policies: [
                ['isLoggedIn', 'isAnAdmin'], // Do these two in parallel
                'onlyInUS' // Then run this policy
            ]
        }
    }
});

A couple of policies are included in the policies folder:

  • isAdmin - Can be used to tag certain users as administrators of the API.
  • isAudit - Used to audit the request and response, stores the results in the local storage database.
  • isIPBlacklist - Used to restrict access to the API for certain IP addresses
  • isIPWhitelist - Used to allow access to the API for certain IP addresses
  • isThrottle - Can be used to throttle API endpoint usage

Proxy Filter

Plugins and Tools used

  • Plugin for setting the request.info.remoteAddress and request.info.remotePort based on the X-Forwarded-For and X-Forwarded-Port headers - therealyou by briandela

How it was implemented in the API Template

This plugin is used for setting the request.info.remoteAddress and request.info.remotePort based on the X-Forwarded-For and X-Forwarded-Port headers. The general format of the x-forwarded-for header is:

X-Forwarded-For: client, proxy1, proxy2

This plugin sets request.info.remoteAddress to the first value of the x-forwarded-for header if it is set.

For example, if the header was

'x-forwarded-for': '192.16.184.5, 192.16.184.6, 192.16.184.2'

then remote.info.remoteAddress would be set to 192.16.184.5

This plugin also sets request.info.remotePort to the value of the x-forwarded-port header

// register plug-ins
server.register([
    Inert,
    Vision,
    Blipp,
	{
		register: require('therealyou')
	},	
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

Monitoring

Plugins and Tools used

How it was implemented in the API Template

This plugin is a simple, self-hosted module based on Socket.IO and Chart.js to report realtime server metrics for hapi.js servers.

Register plugin

server.register({
  register: require('hapijs-status-monitor')
});

Run server and go to /status

Security and Data Encryption

Plugins and Tools used

  • Simple Bearer authentication scheme plugin for hapi - hapi-auth-bearer-token by johnbrett
  • Standard and secure cryptographic algorithms - crypto by Gozala

How it was implemented in the API Template

The template implements a basic bearer token authentication strategy using the hapi-auth-bearer-token plugin, registered in the api_server.js file. Once the plugin is registered, and the server is running, the authentication strategy is available to routes in order to secure individual endpoints. In this instance, the bearer token is compared to an existing user stored in a local database. Any strategy can be implemented by following the same guidelines.

// setup swagger options
const swaggerOptions = {
    info: {
        version: '1',
        title: 'MASTER TEMPLATE',
        description: 'Master API Teplate with cross cutting concerns baked into the template.'
    },
    tags: [
			{
			'name': 'System',
			'description': 'Internal Operations',
			'externalDocs': {
				'description': 'Find out more',
				'url': 'http://www.apeeye.com'
				}
			}, 
			{
			'name': 'User',
			'description': 'Consumer Operations',
			'externalDocs': {
				'description': 'Find out more',
				'url': 'http://www.apeeye.com'
				}
			}, 
			{
			'name': 'TestCases',
			'description': 'Verification',
			'externalDocs': {
				'description': 'Find out more',
				'url': 'http://www.apeeye.com'
				}
			}
		],
	securityDefinitions: {
        'Bearer': {
            'type': 'apiKey',
            'name': 'Authorization',
            'in': 'header',
            'x-keyPrefix': 'Bearer '
        }
    },
    security: [{ 'Bearer': [] }]
};

// register plug-ins
server.register([
    Inert,
    Vision,
    Blipp,
	{
	 register: require('hapi-auth-bearer-token') 
	},	
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

	// Create a validation function for strategy
	var validate = function (token, callback) {
	var fulltoken = 'Bearer ' + token;	
	console.log('Received Token is: ' + token)

	db = new sqlite3.Database('./db/APEEYEDB.db');	
	  var params = [fulltoken]; 
	  var select = 'select * from apeusers where btoken = ?';
	 
	  db.all(select,params, function (err, rows) {
		if(err){
			console.log('ERROR on db read')
			console.log(err);
				console.log('Token Valid');
				callback(null, true, { token: token })
		}else{
			 var reqResponse ;
			console.log('no error on db read')
			console.log(rows.length) 
			console.log(rows) 
		   if (rows.length <= 0){
			 reqResponse = {
			'body' : reqResponse,
			'details' : 'failure'
			}	
			console.log('Token Invalid');
			callback(null, false)				
		   } else {
		 reqResponse = {
		'body' : {'TokenType': 'Bearer', 'Token': rows[0].btoken, 'IncludeInRequestHeader': 'Authorization: ' + rows[0].btoken},
		'details' : 'success'
		}
		console.log(reqResponse);
		callback(null, true, { token: token })				
		   }	
 
		}
	  });
	db.close()		
 
	};
	 server.auth.strategy('simple', 'bearer-access-token', {
			validateFunc: validate
		});

Once the plugin is registered, the routes.js file can be edited to add the authentication strategy to individual endpoints.

	{
	method: 'GET',
	path: '/System/API_Ping/',
	config: {
	auth: {strategies:['simple']},
	   plugins: {
			disinfect: {
				disinfectQuery: true,
				disinfectParams: true,
				disinfectPayload: true
			},
			'hapi-geo-locate': {
				enabled: false
			},
				policies: ['isAdmin', 'isIPWhitelist', 'isThrottle', 'isIPBlacklist']
		},
	handler:  systemEndpoints.API_Ping,
	description: 'API Heartbeat Monitoring',
	notes : 'Endpoint used for Heartbeat Monitoring. Monitoring will use this endpoint to check if the API is up and available.',
	tags: ['api']
		},
	}

For encrypting or decrypting values, there are associated helper functions in the path_handlers/utilities.js file. Encrypting and decrypting values is facilitated by the crypto npm module.

 encrypt: function(text){
  var cipher = crypto.createCipher(algorithm,password)
  var crypted = cipher.update(text,'utf8','hex')
  crypted += cipher.final('hex');
  return crypted;
},

 decrypt: function(text){
  var decipher = crypto.createDecipher(algorithm,password)
  var dec = decipher.update(text,'hex','utf8')
  dec += decipher.final('utf8');
  return dec;
 },		

Storage

Plugins and Tools used

  • Asynchronous, non-blocking SQLite3 bindings for Node.js to store token and user information - node-sqlite3 by mapbox

How it was implemented in the API Template

Within the db folder are two sqlite database instances. One for general information, and one for auditing purposed. Auditing is implemented through the isAudit policy as mentioned above. Should and endpoint request or response require auditing, the isAudit policy needs to be added into the policy configuration of the specified endpoint.

Using the node-sqlite3 module, the following simple database operations are possible.

var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('./db/APEEYEDB.db');

db.serialize(function() {
db.run("DROP TABLE tblUsers");
db.run("CREATE TABLE tblUsers (email TEXT, pass TEXT, btoken TEXT)")
	  var stmt = db.prepare("INSERT INTO tblUsers VALUES (?, ?, ?)");
		stmt.run('someone@email.com', 'yoursupersecretpassword', 'Bearer 12314-12314-12314-12314-12314');
    stmt.finalize();
 
  db.each("SELECT rowid AS id, email, pass, btoken  FROM tblUsers", function(err, row) {
	  console.log(row)
      console.log(row.id + ": " + row.email + ' ' + row.pass + ' ' + row.btoken);
  });
});
 
db.close();

An optional extra feature you could implement is to encrypt the sqlite database at rest using sqlcipher

CRON

Plugins and Tools used

  • A hapi plugin to setup cron jobs - hapi-cron by antonsamper

How it was implemented in the API Template

This plugin is used to setup cron jobs that will call predefined server routes at specified times, for example, the below CRON calls the API_Ping endpoint every 59 seconds. CRON Jobs can be used to automate certain functions like archiving logs files etc.

// register plug-ins
server.register([
    Inert,
    Vision,
    Blipp,
	{
	register: require('hapi-cron'),
	options: {
	 jobs: [{
		name: 'testcron',
		time: '59 * * * * * ',
		timezone: 'Africa/Johannesburg',
		request: {
			headers: {'Authorization': 'Bearer d294b4b6-4d65-4ed8-808e-26954168ff48'},
			method: 'GET',
			url: 'http://localhost:8082/System/API_Ping/'
		},
		callback: (res) => {
			console.log(res.result)
			console.info('testcron has run!');
		}
	}] 
	}
	},
    ], function (err) {

        server.start(function(){
            console.log('Server running at:', server.info.uri);
			
        });
    });

Configuration

Plugins and Tools used

  • Hierarchical configurations for your app deployments - config by lorenwest

How it was implemented in the API Template

Within the config folder, a default.json file exists to store API wide configurations, or any key/values.

Accessing values in the config file is relatively straightforward.

const config = require('config');

var param = config.get('KEY.VALUE'),

Utilities

Plugins and Tools used

  • Full featured promise library - bluebird by petkaantonov
  • Simplest way possible to make http calls - request by request
  • A hapi plugin to geo locate requests - hapi-geo-locate by futurestud.io
  • Simple server-side session support for hapi - hapi-server-session by btmorex
  • User-agent information plugin for hapi - scooter by hapijs
  • Async utilities for node - async by caolan
  • Simple, fast generation of RFC4122 UUIDS - node-uuid by kelektiv

SDK Generation

Plugins and Tools used

  • SDK Generation through Swagger (OpenAPI) Specification code generator featuring C# and Razor templates. Supports C#, Java, Node.js, TypeScript, Python and Ruby - autorest by Azure

Logging

Plugins and Tools used

  • Logging capabilities provided by pm2 by keymetrics

How it was implemented in the API Template

Within the startup.json file, the locations for the log files can be specified.

Load Balancing

Plugins and Tools used

  • Load Balancing capabilities provided by pm2 by keymetrics

How it was implemented in the API Template

Within the startup.json file, the number of API instance can be specified.