estate/bookshelf-bcrypt

View on GitHub
index.js

Summary

Maintainability
C
7 hrs
Test Coverage
'use strict'
 
const merge = require('lodash.merge')
const get = require('lodash.get')
const bcrypt = require('bcrypt')
 
// https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016
const RECOMMENDED_ROUNDS = 12
 
Function `exports` has 73 lines of code (exceeds 25 allowed). Consider refactoring.
Function `exports` has a Cognitive Complexity of 20 (exceeds 5 allowed). Consider refactoring.
module.exports = (bookshelf, settings) => {
const BookshelfModel = bookshelf.Model
 
// Add default settings
settings = merge({
allowEmptyPassword: false,
rounds: RECOMMENDED_ROUNDS,
detectBcrypt: () => false,
onRehash: function () {
throw new this.constructor.BcryptRehashDetected()
}
}, settings)
 
/**
* Hashes a string and stores it inside the provided model
* @param {String} string A string to be hashed
* @param {String} field The field that will receive the hashed string
* @param {Object} model An instantiated bookshelf model
* @throws {BcryptRehashDetected} If it detects a rehash
* @return {Promise} A promise that resolves with the model correctly updated
*/
function hashAndStore (string, field, model) {
return new Promise(function (resolve, reject) {
// Avoid rehashing a string by mistake but allow users to implement
// non throwing logic
if (settings.detectBcrypt(string) && typeof settings.onRehash === 'function') {
try {
settings.onRehash.call(model)
} catch (err) {
return reject(err)
}
}
 
bcrypt.hash(string, settings.rounds, (err, hash) => {
if (err) return reject(err)
 
// Set the field and resolves the promise
model.set(field, hash)
resolve(model)
})
})
}
 
/**
* Compares a string against a bcrypt hash
* @param {String} str The raw string to be compared
* @param {String} hash A bcrypt hash to match against the string
* @return {Promise} A promise that resolves to a boolean indicating if the
* hash was generated from the provided string
*/
function compare (str, hash) {
return new Promise(function (resolve, reject) {
bcrypt.compare(str, hash, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
}
 
/**
* Custom error class for throwing when this plugin detects a rehash
* @type {Error}
*/
Similar blocks of code found in 2 locations. Consider refactoring.
bookshelf.BcryptRehashDetected = BookshelfModel.BcryptRehashDetected = class extends Error {
constructor () {
super('Bcrypt tried to hash another bcrypt hash')
this.name = 'BcryptRehashDetected'
}
}
 
/**
* Custom error class for throwing when this plugin detects a null or undefined password
* @type {Error}
*/
Similar blocks of code found in 2 locations. Consider refactoring.
bookshelf.EmptyPasswordDetected = BookshelfModel.EmptyPasswordDetected = class extends Error {
constructor () {
super('Bcrypt cannot hash a null or undefined password')
this.name = 'EmptyPasswordDetected'
}
}
 
// Extends the default model class
bookshelf.Model = bookshelf.Model.extend({}, {
extended (child) {
// Check if the extended model has the bcrypt option
const field = get(child.prototype, 'bcrypt.field')
 
// Configure bcrypt only for enabled models
if (field) {
const initialize = child.prototype.initialize
 
child.prototype.initialize = function () {
// Do not override child's initialization
if (initialize) initialize.call(this)
 
// Hash the password when saving
this.on('saving', (model, attrs, options) => {
const field = get(this, 'bcrypt.field')
 
if (model.hasChanged(field) && options.bcrypt !== false) {
const password = model.get(field)
 
if (password !== null && typeof password !== 'undefined') {
return hashAndStore(password, field, model)
} else if (this.bcrypt.allowEmptyPassword !== true) {
throw new this.constructor.EmptyPasswordDetected()
}
}
})
}
 
/**
* Compares a string against a bcrypt hash stored in the current model
* @param {String} str The string to compare against the hash
* @return {Promise} A promise that resolves to a boolean indicating if
* the provided string is valid or not
*/
child.prototype.compare = function (str) {
return compare(str, this.get(field))
}
}
}
})
}