diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index f44565d61d..d7e0d6e0d2 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -2,6 +2,7 @@ "invalid-data": "Invalid Data", "not-logged-in": "You don't seem to be logged in.", + "account-locked": "Your account has been locked temporarily", "invalid-cid": "Invalid Category ID", "invalid-tid": "Invalid Topic ID", diff --git a/public/src/forum/login.js b/public/src/forum/login.js index 5741cb0079..632844560d 100644 --- a/public/src/forum/login.js +++ b/public/src/forum/login.js @@ -45,7 +45,11 @@ define(function() { app.loadConfig(); }, error: function(data, textStatus, jqXHR) { - $('#login-error-notify').show(); + // Update error text + translator.translate(data.responseJSON, function(errorText) { + $('#login-error-notify').show().html(errorText); + }); + $('#login').removeAttr('disabled').html('Login'); }, dataType: 'json', diff --git a/src/database/level/main.js b/src/database/level/main.js index a147803029..bbde367fc9 100644 --- a/src/database/level/main.js +++ b/src/database/level/main.js @@ -64,6 +64,10 @@ module.exports = function(db, module) { } }; + module.increment = function(key, callback) { + // ^_^ + }; + module.rename = function(oldKey, newKey, callback) { // G__G }; diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 834ad4e150..d7c41e4cb9 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -79,6 +79,10 @@ module.exports = function(db, module) { module.setObject(key, data, callback); }; + module.increment = function(key, callback) { + db.collection('objects').update({_key: key}, { $inc: { value: 1 } }, helpers.done(callback)); + }; + module.rename = function(oldKey, newKey, callback) { db.collection('objects').update({_key: oldKey}, {$set:{_key: newKey}}, helpers.done(callback)); }; diff --git a/src/database/redis/main.js b/src/database/redis/main.js index 2525b4972f..caeb6ab70b 100644 --- a/src/database/redis/main.js +++ b/src/database/redis/main.js @@ -85,6 +85,10 @@ module.exports = function(redisClient, module) { redisClient.set(key, value, callback); }; + module.increment = function(key, callback) { + redisClient.incr(key, callback); + }; + module.rename = function(oldKey, newKey, callback) { redisClient.rename(oldKey, newKey, callback); }; @@ -101,7 +105,7 @@ module.exports = function(redisClient, module) { redisClient.pexpire(key, ms, callback); }; - module.expireAt = function(key, timestamp, callback) { + module.pexpireAt = function(key, timestamp, callback) { redisClient.pexpireat(key, timestamp, callback); }; }; \ No newline at end of file diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 53d1953bec..131ab47ec0 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -171,35 +171,44 @@ } if(!uid) { - // Even if a user doesn't exist, compare passwords anyway, so we don't immediately return + // To-do: Even if a user doesn't exist, compare passwords anyway, so we don't immediately return return next(null, false, '[[error:no-user]]'); } - db.getObjectFields('user:' + uid, ['password', 'banned'], function(err, userData) { + user.auth.logAttempt(uid, function(err) { if (err) { - return next(err); + return next(null, false, err.message); } - if (!userData || !userData.password) { - return next(new Error('[[error:invalid-user-data]]')); - } - - if (userData.banned && parseInt(userData.banned, 10) === 1) { - return next(null, false, '[[error:user-banned]]'); - } - - bcrypt.compare(password, userData.password, function(err, res) { + db.getObjectFields('user:' + uid, ['password', 'banned'], function(err, userData) { if (err) { - return next(new Error('bcrypt compare error')); + return next(err); } - if (!res) { - return next(null, false, '[[error:invalid-password]]'); + if (!userData || !userData.password) { + return next(new Error('[[error:invalid-user-data]]')); } - next(null, { - uid: uid - }, '[[success:authentication-successful]]'); + if (userData.banned && parseInt(userData.banned, 10) === 1) { + return next(null, false, '[[error:user-banned]]'); + } + + bcrypt.compare(password, userData.password, function(err, res) { + if (err) { + return next(new Error('bcrypt compare error')); + } + + if (!res) { + return next(null, false, '[[error:invalid-password]]'); + } + + // Clear login attempts + user.auth.clearLoginAttempts(uid); + + next(null, { + uid: uid + }, '[[success:authentication-successful]]'); + }); }); }); }); diff --git a/src/user.js b/src/user.js index 029897383e..077a1e3d0f 100644 --- a/src/user.js +++ b/src/user.js @@ -23,6 +23,7 @@ var bcrypt = require('bcryptjs'), User.notifications = require('./user/notifications'); User.reset = require('./user/reset'); + require('./user/auth')(User); require('./user/create')(User); require('./user/follow')(User); require('./user/profile')(User); diff --git a/src/user/auth.js b/src/user/auth.js new file mode 100644 index 0000000000..71de297ca0 --- /dev/null +++ b/src/user/auth.js @@ -0,0 +1,32 @@ +var db = require('../database'), + meta = require('../meta'); + +module.exports = function(User) { + User.auth = {}; + + User.auth.logAttempt = function(uid, callback) { + db.exists('lockout:' + uid, function(err, exists) { + if (!exists) { + db.increment('loginAttempts:' + uid, function(err, attempts) { + if ((meta.config.loginAttempts || 5) < attempts) { + // Lock out the account + db.set('lockout:' + uid, '', function(err) { + db.delete('loginAttempts:' + uid); + db.pexpire('lockout:' + uid, 1000*60*(meta.config.lockoutDuration || 60)); + callback(new Error('account-locked')); + }); + } else { + db.pexpire('loginAttempts:' + uid, 1000*60*60); + callback(); + } + }); + } else { + callback(new Error('[[error:account-locked]]')); + } + }) + }; + + User.auth.clearLoginAttempts = function(uid) { + db.delete('loginAttempts:' + uid); + }; +}; \ No newline at end of file