diff --git a/README.md b/README.md index ccd5855..b167135 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ contains an `ok` field that is either true or false, indicating the success or f * `mustrepeat`: you must provide a `repeat` argument (a repeat of the password) when setting a password * `resetperiod`: duration in millis that a password reset token is valid, default: 24 hours * `pepper`: used in addition to password salts, a pepper is very similar but is stored in code instead of a database. + * `lockTry`: number of failed attempts before the account is locked, default 3, set to null to disable this feature To set options, do so when you load the plugin: @@ -430,6 +431,19 @@ Object with properties: * `ok`: true if operation is OK +### role:user, cmd:set_user_lock + +Sets the lock on users account + +#### Argumants: + + * `id`: the id of the user + * `failTry`: boolean to say if the login failed or not + * `why`: why the accouunts lock status is being changed + +#### Provides: + + * `ok`: true if the account was unloocked or false if the lock was incremented ## Logging diff --git a/default-options.json b/default-options.json index b38d403..8558fae 100644 --- a/default-options.json +++ b/default-options.json @@ -7,6 +7,7 @@ "confirm": false, "oldsha": true, "pepper": "", + "lockTry": null, "user": { "fields": [ { diff --git a/test/lock-unlock.test.js b/test/lock-unlock.test.js new file mode 100644 index 0000000..e6fa47b --- /dev/null +++ b/test/lock-unlock.test.js @@ -0,0 +1,148 @@ +'use strict' + +var Seneca = require('seneca') + +var _ = require('lodash') + +var Lab = require('lab') +var Code = require('code') +var lab = exports.lab = Lab.script() +var suite = lab.suite +var test = lab.test +var before = lab.before +var expect = Code.expect +var wrongPassword = 'fail1fail' +var resetPassword = 'reset1reset' + +var si = Seneca() +if (si.version >= '2.0.0'){ + si + .use(require('seneca-entity')) +} +si.use('../user', {lockTry: 3}) + +var user1Data = { + nick: 'nick1', + email: 'nick1@example.com', + password: 'test1test', + active: true, +} + +var user2Data = { + nick: 'nick2', + email: 'nick2@example.com', + password: 'test2test', + active: true, +} + +suite('seneca-user update suite tests ', function () { + before({}, function (done) { + si.ready(function (err) { + if (err) return process.exit(!console.error(err)) + done() + }) + }) + + test('user/register test', function (done) { + si.act(_.extend({role: 'user', cmd: 'register'}, user1Data), function (err, data) { + expect(err).to.not.exist() + expect(data.user.nick).to.equal(user1Data.nick) + done(err) + }) + }) + + test('user/register test', function (done) { + si.act(_.extend({role: 'user', cmd: 'register'}, user2Data), function (err, data) { + expect(err).to.not.exist() + expect(data.user.nick).to.equal(user2Data.nick) + done(err) + }) + }) + + var user2Id + test('user/login user test', function (done) { + si.act({role: 'user', cmd: 'login', nick: user2Data.nick, password: user2Data.password}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.true() + user2Id = data.user.id + done(err) + }) + }) + + for(var i=0; i<3; i++) { + test('user/login user test', function (done) { + si.act({role: 'user', cmd: 'login', nick: user1Data.nick, password: wrongPassword}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.false() + done(err) + }) + }) + + test('user/user lock test', function (done) { + si.act({role: 'user', cmd: 'set_user_lock', id: user2Id, failTry: true}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.false() + done(err) + }) + }) + } + + test('user/login user test', function (done) { + si.act({role: 'user', cmd: 'login', nick: user1Data.nick, password: user1Data.password}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.false() + done(err) + }) + }) + + test('user/login user test', function (done) { + si.act({role: 'user', cmd: 'login', nick: user2Data.nick, password: user2Data.password}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.false() + done(err) + }) + }) + + test('user/user unlock test', function (done) { + si.act({role: 'user', cmd: 'set_user_lock', id: user2Id}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.true() + done(err) + }) + }) + + var resetId + test('user/create_reset unknown user test', function (done) { + si.act({ role: 'user', cmd: 'create_reset', nick: user1Data.nick }, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.true() + expect(data.reset.id).to.exist() + resetId = data.reset.token + done(err) + }) + }) + + test('user/create_reset unknown user test', function (done) { + si.act({ role: 'user', cmd: 'execute_reset', token: resetId, password: resetPassword, repeat: resetPassword }, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.true() + done(err) + }) + }) + + test('user/login user test', function (done) { + si.act({role: 'user', cmd: 'login', nick: user1Data.nick, password: resetPassword}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.true() + done(err) + }) + }) + + test('user/login user test', function (done) { + si.act({role: 'user', cmd: 'login', nick: user2Data.nick, password: user2Data.password}, function (err, data) { + expect(err).to.not.exist() + expect(data.ok).to.be.true() + done(err) + }) + }) +}) diff --git a/user.js b/user.js index 0a79513..9041632 100644 --- a/user.js +++ b/user.js @@ -219,6 +219,12 @@ module.exports = function user (options) { cmd_update) + // ### Set user lock + // Pattern: _**role**:user, **cmd**:set_user_lock_ + seneca.add({role: role, cmd: 'set_user_lock'}, + cmd_set_user_lock) + + // ### Enable User - DEPRECATED // Replaced with **activate** command seneca.add({role: role, cmd: 'enable'}, // keep this for backward compatibility @@ -485,6 +491,7 @@ module.exports = function user (options) { return done(null, out) } + user.lockTry = 0 user.salt = out.salt user.pass = out.pass user.save$(function (err, user) { @@ -526,6 +533,7 @@ module.exports = function user (options) { user.name = args.name || '' user.active = void 0 === args.active ? true : args.active user.when = new Date().toISOString() + user.lockTry = 0; if (options.confirm) { user.confirmed = args.confirmed || false @@ -612,7 +620,7 @@ module.exports = function user (options) { // - password: password text, alias: pass // Provides: // - success: {ok:true,user:,login:} - // - failure: {ok:false,why:,nick:} + // - failure: {ok:false,why:,user:} function cmd_login (args, done) { var seneca = this var user = args.user @@ -625,6 +633,10 @@ module.exports = function user (options) { return done(null, {ok: false, why: why, user: user}) } + if (user.lockTry >= options.lockTry && !_.isNull(options.lockTry)) { + seneca.log.debug('login/fail', why = 'locked-out', user) + return done(null, {ok: false, why: why, user: user}) + } if (args.auto) { return make_login(user, 'auto') } @@ -633,7 +645,10 @@ module.exports = function user (options) { if (err) return done(err) if (!out.ok) { seneca.log.debug('login/fail', why = 'invalid-password', user) - return done(null, {ok: false, why: why}) + seneca.act({role: role, cmd: 'set_user_lock', id: user.id, failTry: true, why: why},function (err, out) { + if (err) return done(err) + done(null, {ok: false, why: why, user:user}) + }) } else return make_login(user, 'password') }) @@ -660,13 +675,47 @@ module.exports = function user (options) { login.save$(function (err, login) { if (err) return done(err) - - seneca.log.debug('login/ok', why, user, login) - done(null, {ok: true, user: user, login: login, why: why}) + seneca.act({role: role, cmd: 'set_user_lock', id: user.id}, function (err, out) { + if (err) return done(err) + seneca.log.debug('login/ok', why, user, login) + done(null, {ok: true, user: user, login: login, why: why}) + }) }) } } + // Unlocks accounts that a user has been locked out of due to failled password attempts + // Or let the lock if the user failed to login to their account + // - id, user id to unlock + // - failTry, boolean to say login failed + // - why, why account lock was changed + // Provides: + // - success: {ok:true, why:} + // - failure: {ok:false, why:} + function cmd_set_user_lock (args, done) { + var seneca = this + var why + var userent = seneca.make(user_canon) + var ok + + userent.load$({id: args.id}, function (err, user) { + if (err) return done(err, {ok: false, why: err}) + if(!_.isUndefined(args.failTry) && args.failTry) { + user.lockTry = user.lockTry + 1 + ok = false + why = args.why + } else { + user.lockTry = 0 + ok = true + why = 'account-unlocked' + } + user.save$(function (err, user) { + if (err) return done(err, {ok: false, why: err}) + seneca.log.debug('user/lock', why, user) + done(null, {ok: ok, why: why}) + }) + }) + } // Confirm an existing user - using confirm code sent to user // - nick, email: to resolve user