diff --git a/.travis.yml b/.travis.yml index 34f1462..819b04c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,14 @@ language: node_js node_js: - "6" +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 script: npm run coverage before_script: - psql -c 'CREATE DATABASE travis_ci_test;' -U postgres diff --git a/lib/config_validator.js b/lib/config_validator.js index d5c80f7..f1142d4 100644 --- a/lib/config_validator.js +++ b/lib/config_validator.js @@ -14,8 +14,10 @@ var fieldSchema = Joi.object() ; var configSchema = Joi.object().keys({ table_name: Joi.string().regex(dbNameRegEx).required(), // eslint-disable-line - fields: Joi.object().pattern(dbNameRegEx, fieldSchema).required() // eslint-disable-line -}); + fields: Joi.object().pattern(dbNameRegEx, fieldSchema).required(), // eslint-disable-line + salt_number: Joi.number().integer().min(1) // eslint-disable-line +}) + .unknown(); module.exports = function (config) { return Joi.assert(config, configSchema); diff --git a/lib/create_table_map.js b/lib/create_table_map.js index 55855f4..9ecab24 100644 --- a/lib/create_table_map.js +++ b/lib/create_table_map.js @@ -4,8 +4,13 @@ var mapObj = { number: function (opts) { return opts.integer ? 'BIGINT' : 'DOUBLE PRECISION'; }, - string: function (opts) { + string: function (opts, name) { var length = opts.max || 80; + var willBeHashed = name === 'password'; + + if (willBeHashed) { + length = 60; // http://stackoverflow.com/questions/5881169/what-column-type-length-should-i-use-for-storing-a-bcrypt-hashed-password-in-a-d + } return 'VARCHAR(' + length + ')'; }, @@ -25,7 +30,7 @@ function mapper (name, type, options) { constraints += ' CONSTRAINT ' + name + '_unique UNIQUE'; } - return name + ' ' + mapObj[type](opts) + constraints; + return name + ' ' + mapObj[type](opts, name) + constraints; } module.exports = mapper; diff --git a/lib/db.js b/lib/db.js index bfdc728..2bc77a3 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,15 +1,39 @@ 'use strict'; +var bcrypt = require('bcrypt'); + var sqlGen = require('./sql_gen.js'); var configValidator = require('./config_validator.js'); +var utils = require('./utils.js'); + +var methods = { + init: function (client, config, _, cb) { + configValidator(config); + + return client.query(sqlGen.init(config), cb); + }, + insert: function (client, config, options, cb) { + var passwordValue = options.fields.password; + var saltNumber = config.salt_number || 10; + var tableName = config.table_name; + var makeQuery = function (opts) { + var args = sqlGen.insert(tableName, opts).concat([cb]); + + return client.query.apply(client, args); + }; + + return bcrypt.hash(passwordValue, saltNumber, function (_, hash) { + var optionsCopy = utils.shallowCopy(options); -var methods = { init: function (client, config, _, cb) { - configValidator(config); + optionsCopy.fields = utils.shallowCopy(optionsCopy.fields); + optionsCopy.fields.password = hash; - return client.query(sqlGen.init(config), cb); -} }; + return makeQuery(optionsCopy); + }); + } +}; -['select', 'update', 'delete', 'insert'].forEach(function (method) { +['select', 'update', 'delete'].forEach(function (method) { methods[method] = function (client, config, options, cb) { var tableName = config.table_name; var args = sqlGen[method](tableName, options).concat([cb]); diff --git a/package.json b/package.json index c6604bb..7dd8b2a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "coverage" ], "dependencies": { + "bcrypt": "^0.8.7", "env2": "^2.1.1", "hoek": "^4.1.0", "joi": "^9.0.4", diff --git a/test/config_validator.test.js b/test/config_validator.test.js index 2835015..71b06b9 100644 --- a/test/config_validator.test.js +++ b/test/config_validator.test.js @@ -39,8 +39,9 @@ test('config validator', function (t) { table_name: 'test', // eslint-disable-line fields: { email: { type: 'string', - unknown: 'allowed' - } } + unknown: 'field' + } }, + unknown: 'config' }), 'no error when extra options unknown' ); @@ -48,6 +49,35 @@ test('config validator', function (t) { t.end(); }); +test('salt_number', function (t) { + t.doesNotThrow( + validator({ + table_name: 'test', // eslint-disable-line + fields: { }, + salt_number: 1 // eslint-disable-line + }), + '1 min number of rounds' + ); + t.throws( + validator({ + table_name: 'test', // eslint-disable-line + fields: { }, + salt_number: 1.4 // eslint-disable-line + }), + 'salt number must be integer' + ); + t.throws( + validator({ + table_name: 'test', // eslint-disable-line + fields: { }, + salt_number: 0 // eslint-disable-line + }), + 'salt number higher than 0' + ); + + t.end(); +}); + test('dbNameRegEx', function (t) { t.ok( dbNameRegEx.exec('_a1pha_Numer1c'), diff --git a/test/db.test.js b/test/db.test.js index 780c5db..b6e2554 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -1,6 +1,7 @@ 'use strict'; var test = require('tape'); +var bcrypt = require('bcrypt'); var client = require('./test_pg_client.js'); var db = require('../lib/db.js'); @@ -9,7 +10,8 @@ var schema = require('./example_schema.js'); var testInsert = { email: 'test@gmail.com', dob: '2001-09-27', - username: 'test' + username: 'test', + password: 'hash me' }; test('init test client', function (t) { @@ -39,48 +41,78 @@ test('db.init', function (t) { test('db.insert & default select w custom where', function (t) { - db.insert(client, schema, { fields: testInsert }) - .then(function () { - return db.select(client, schema, { where: { dob: '2001-09-27' } }); - }) - .then(function (res) { - t.equal( - res.rows[0].email, - testInsert.email, - 'email correct' - ); - t.equal( - res.rows[0].username, - testInsert.username, - 'username correct' - ); - t.equal( - res.rows[0].dob.toLocaleDateString('GMT'), - new Date(testInsert.dob).toLocaleDateString('GMT'), - 'get same date back, though now a date object' - ); - t.end(); - }) - .catch(function (err) { - t.fail(err); - t.end(); - }) - ; + db.insert(client, schema, { fields: testInsert }, function () { + return db.select(client, schema, { where: { dob: '2001-09-27' } }) + .then(function (res) { + t.equal( + res.rows[0].email, + testInsert.email, + 'email correct' + ); + t.equal( + res.rows[0].username, + testInsert.username, + 'username correct' + ); + t.ok( + bcrypt.compareSync(testInsert.password, res.rows[0].password), + 'password hashed correctly' + ); + t.equal( + res.rows[0].dob.toLocaleDateString('GMT'), + new Date(testInsert.dob).toLocaleDateString('GMT'), + 'get same date back, though now a date object' + ); + t.end(); + }) + .catch(function (err) { + t.fail(err); + t.end(); + }) + ; + }); +}); + +test('db.insert & default select w custom where', function (t) { + db.insert(client, schema, { fields: testInsert }, function () { + return db.select(client, schema, { where: { dob: '2001-09-27' } }) + .then(function (res) { + t.equal( + res.rows[0].email, + testInsert.email, + 'email correct' + ); + t.equal( + res.rows[0].username, + testInsert.username, + 'username correct' + ); + t.ok( + bcrypt.compareSync(testInsert.password, res.rows[0].password), + 'password hashed correctly' + ); + t.equal( + res.rows[0].dob.toLocaleDateString('GMT'), + new Date(testInsert.dob).toLocaleDateString('GMT'), + 'get same date back, though now a date object' + ); + t.end(); + }) + .catch(function (err) { + t.fail(err); + t.end(); + }) + ; + }); }); test('db.insert x 2 same username error', function (t) { t.plan(1); - db.insert(client, schema, { fields: testInsert }) - .then(function () { - return db.insert(client, schema, { fields: testInsert }); - }) - .then(function () { - t.fails('shouldn\'t allow second insert if unique key given'); - }) - .catch(function () { - t.pass('shouldn\'t allow second insert if unique key given'); - }) - ; + db.insert(client, schema, { fields: testInsert }, function () { + return db.insert(client, schema, { fields: testInsert }, function (err) { + t.ok(err, 'shouldn\'t allow second insert if unique key given'); + }); + }); }); test('db.update w where & custom select w default where', function (t) { diff --git a/test/example_schema.js b/test/example_schema.js index 8b78c70..f029993 100644 --- a/test/example_schema.js +++ b/test/example_schema.js @@ -13,6 +13,11 @@ module.exports = { max: 20, unique: true }, + password: { + type: 'string', + min: 5, + max: 20 + }, dob: { type: 'date' } } }; diff --git a/test/sql_gen.test.js b/test/sql_gen.test.js index d65e5fe..b2b8393 100644 --- a/test/sql_gen.test.js +++ b/test/sql_gen.test.js @@ -20,9 +20,10 @@ tape('::init - generate SQL to create a table if none exists', function (t) { 'CREATE TABLE IF NOT EXISTS "user_data" (' + 'email VARCHAR(80), ' + 'username VARCHAR(20) CONSTRAINT username_unique UNIQUE, ' + + 'password VARCHAR(60), ' + 'dob DATE' + ')', - 'Create table query generation from config object' + 'Create table query generation from config object, pw length overwritten' ); t.end(); });