|
|
|
'use strict';
|
|
|
|
|
|
|
|
var async = require('async');
|
|
|
|
var winston = require('winston');
|
|
|
|
var db = require('../database');
|
|
|
|
var meta = require('../meta');
|
|
|
|
var events = require('../events');
|
|
|
|
var batch = require('../batch');
|
|
|
|
|
|
|
|
module.exports = function (User) {
|
|
|
|
User.auth = {};
|
|
|
|
|
|
|
|
User.auth.logAttempt = function (uid, ip, callback) {
|
|
|
|
if (!parseInt(uid, 10)) {
|
|
|
|
return setImmediate(callback);
|
|
|
|
}
|
|
|
|
async.waterfall([
|
|
|
|
function (next) {
|
|
|
|
db.exists('lockout:' + uid, next);
|
|
|
|
},
|
|
|
|
function (exists, next) {
|
|
|
|
if (exists) {
|
|
|
|
return callback(new Error('[[error:account-locked]]'));
|
|
|
|
}
|
|
|
|
db.increment('loginAttempts:' + uid, next);
|
|
|
|
},
|
|
|
|
function (attemps, next) {
|
|
|
|
var loginAttempts = parseInt(meta.config.loginAttempts, 10) || 5;
|
|
|
|
if (attemps <= loginAttempts) {
|
|
|
|
return db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60, callback);
|
|
|
|
}
|
|
|
|
// Lock out the account
|
|
|
|
db.set('lockout:' + uid, '', next);
|
|
|
|
},
|
|
|
|
function (next) {
|
|
|
|
var duration = 1000 * 60 * (meta.config.lockoutDuration || 60);
|
|
|
|
|
|
|
|
db.delete('loginAttempts:' + uid);
|
|
|
|
db.pexpire('lockout:' + uid, duration);
|
|
|
|
events.log({
|
|
|
|
type: 'account-locked',
|
|
|
|
uid: uid,
|
|
|
|
ip: ip,
|
|
|
|
});
|
|
|
|
next(new Error('[[error:account-locked]]'));
|
|
|
|
},
|
|
|
|
], callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.clearLoginAttempts = function (uid) {
|
|
|
|
db.delete('loginAttempts:' + uid);
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.resetLockout = function (uid, callback) {
|
|
|
|
async.parallel([
|
|
|
|
async.apply(db.delete, 'loginAttempts:' + uid),
|
|
|
|
async.apply(db.delete, 'lockout:' + uid),
|
|
|
|
], callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.getSessions = function (uid, curSessionId, callback) {
|
|
|
|
var _sids;
|
|
|
|
|
|
|
|
// curSessionId is optional
|
|
|
|
if (arguments.length === 2 && typeof curSessionId === 'function') {
|
|
|
|
callback = curSessionId;
|
|
|
|
curSessionId = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
async.waterfall([
|
|
|
|
async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':sessions', 0, 19),
|
|
|
|
function (sids, next) {
|
|
|
|
_sids = sids;
|
|
|
|
async.map(sids, db.sessionStore.get.bind(db.sessionStore), next);
|
|
|
|
},
|
|
|
|
function (sessions, next) {
|
|
|
|
sessions.forEach(function (sessionObj, idx) {
|
|
|
|
if (sessionObj && sessionObj.meta) {
|
|
|
|
sessionObj.meta.current = curSessionId === _sids[idx];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Revoke any sessions that have expired, return filtered list
|
|
|
|
var expiredSids = [];
|
|
|
|
var expired;
|
|
|
|
|
|
|
|
sessions = sessions.filter(function (sessionObj, idx) {
|
|
|
|
expired = !sessionObj || !sessionObj.hasOwnProperty('passport') ||
|
|
|
|
!sessionObj.passport.hasOwnProperty('user') ||
|
|
|
|
parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10);
|
|
|
|
|
|
|
|
if (expired) {
|
|
|
|
expiredSids.push(_sids[idx]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return !expired;
|
|
|
|
});
|
|
|
|
|
|
|
|
async.each(expiredSids, function (sid, next) {
|
|
|
|
User.auth.revokeSession(sid, uid, next);
|
|
|
|
}, function (err) {
|
|
|
|
next(err, sessions);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
], function (err, sessions) {
|
|
|
|
callback(err, sessions ? sessions.map(function (sessObj) {
|
|
|
|
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
|
|
|
|
return sessObj.meta;
|
|
|
|
}) : undefined);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.addSession = function (uid, sessionId, callback) {
|
|
|
|
callback = callback || function () {};
|
|
|
|
db.sortedSetAdd('uid:' + uid + ':sessions', Date.now(), sessionId, callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.revokeSession = function (sessionId, uid, callback) {
|
|
|
|
winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid);
|
|
|
|
|
|
|
|
db.sessionStore.get(sessionId, function (err, sessionObj) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
async.parallel([
|
|
|
|
function (next) {
|
|
|
|
if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) {
|
|
|
|
db.deleteObjectField('uid:' + uid + ':sessionUUID:sessionId', sessionObj.meta.uuid, next);
|
|
|
|
} else {
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
async.apply(db.sortedSetRemove, 'uid:' + uid + ':sessions', sessionId),
|
|
|
|
async.apply(db.sessionStore.destroy.bind(db.sessionStore), sessionId),
|
|
|
|
], callback);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.revokeAllSessions = function (uid, callback) {
|
|
|
|
async.waterfall([
|
|
|
|
async.apply(db.getSortedSetRange, 'uid:' + uid + ':sessions', 0, -1),
|
|
|
|
function (sids, next) {
|
|
|
|
async.each(sids, function (sid, next) {
|
|
|
|
User.auth.revokeSession(sid, uid, next);
|
|
|
|
}, next);
|
|
|
|
},
|
|
|
|
], callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
User.auth.deleteAllSessions = function (callback) {
|
|
|
|
var _ = require('underscore');
|
|
|
|
batch.processSortedSet('users:joindate', function (uids, next) {
|
|
|
|
var sessionKeys = uids.map(function (uid) {
|
|
|
|
return 'uid:' + uid + ':sessions';
|
|
|
|
});
|
|
|
|
|
|
|
|
var sessionUUIDKeys = uids.map(function (uid) {
|
|
|
|
return 'uid:' + uid + ':sessionUUID:sessionId';
|
|
|
|
});
|
|
|
|
|
|
|
|
async.waterfall([
|
|
|
|
function (next) {
|
|
|
|
db.getSortedSetRange(sessionKeys, 0, -1, next);
|
|
|
|
},
|
|
|
|
function (sids, next) {
|
|
|
|
sids = _.flatten(sids);
|
|
|
|
async.parallel([
|
|
|
|
async.apply(db.deleteAll, sessionUUIDKeys),
|
|
|
|
async.apply(db.deleteAll, sessionKeys),
|
|
|
|
function (next) {
|
|
|
|
async.each(sids, function (sid, next) {
|
|
|
|
db.sessionStore.destroy(sid, next);
|
|
|
|
}, next);
|
|
|
|
},
|
|
|
|
], next);
|
|
|
|
},
|
|
|
|
], next);
|
|
|
|
}, { batch: 1000 }, callback);
|
|
|
|
};
|
|
|
|
};
|