diff --git a/src/user/auth.js b/src/user/auth.js index 0adf589967..de2882d187 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -8,146 +8,171 @@ const events = require('../events'); const batch = require('../batch'); const utils = require('../utils'); -module.exports = function (User) { - User.auth = {}; - - User.auth.logAttempt = async function (uid, ip) { - if (!(parseInt(uid, 10) > 0)) { - return; - } - const exists = await db.exists(`lockout:${uid}`); - if (exists) { - throw new Error('[[error:account-locked]]'); - } - const attempts = await db.increment(`loginAttempts:${uid}`); - if (attempts <= meta.config.loginAttempts) { - return await db.pexpire(`loginAttempts:${uid}`, 1000 * 60 * 60); - } - // Lock out the account - await db.set(`lockout:${uid}`, ''); - const duration = 1000 * 60 * meta.config.lockoutDuration; - - await db.delete(`loginAttempts:${uid}`); - await db.pexpire(`lockout:${uid}`, duration); - await events.log({ - type: 'account-locked', - uid: uid, - ip: ip, - }); +async function logAttempt(uid, ip) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const exists = await db.exists(`lockout:${uid}`); + if (exists) { throw new Error('[[error:account-locked]]'); - }; - - User.auth.getFeedToken = async function (uid) { - if (!(parseInt(uid, 10) > 0)) { - return; + } + const attempts = await db.increment(`loginAttempts:${uid}`); + if (attempts <= meta.config.loginAttempts) { + return await db.pexpire(`loginAttempts:${uid}`, 1000 * 60 * 60); + } + // Lock out the account + await db.set(`lockout:${uid}`, ''); + const duration = 1000 * 60 * meta.config.lockoutDuration; + + await db.delete(`loginAttempts:${uid}`); + await db.pexpire(`lockout:${uid}`, duration); + await events.log({ + type: 'account-locked', + uid: uid, + ip: ip, + }); + throw new Error('[[error:account-locked]]'); +} + +async function getFeedToken(User, uid) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const _token = await db.getObjectField(`user:${uid}`, 'rss_token'); + const token = _token || utils.generateUUID(); + if (!_token) { + await User.setUserField(uid, 'rss_token', token); + } + return token; +} + +async function clearLoginAttempts(uid) { + await db.delete(`loginAttempts:${uid}`); +} + +async function resetLockout(uid) { + await db.deleteAll([ + `loginAttempts:${uid}`, + `lockout:${uid}`, + ]); +} + +async function getSessions(uid, curSessionId) { + await cleanExpiredSessions(uid); + const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19); + let sessions = await Promise.all(sids.map(sid => db.sessionStoreGet(sid))); + sessions = sessions.map((sessObj, idx) => { + if (sessObj && sessObj.meta) { + sessObj.meta.current = curSessionId === sids[idx]; + sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); + sessObj.meta.ip = validator.escape(String(sessObj.meta.ip)); } - const _token = await db.getObjectField(`user:${uid}`, 'rss_token'); - const token = _token || utils.generateUUID(); - if (!_token) { - await User.setUserField(uid, 'rss_token', token); + return sessObj && sessObj.meta; + }).filter(Boolean); + return sessions; +} + +async function cleanExpiredSessions(uid) { + const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1); + if (!sids.length) { + return []; + } + + const expiredSids = []; + const activeSids = []; + await Promise.all(sids.map(async (sid) => { + const sessionObj = await db.sessionStoreGet(sid); + const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || + !sessionObj.passport.hasOwnProperty('user') || + parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); + if (expired) { + expiredSids.push(sid); + } else { + activeSids.push(sid); } - return token; - }; + })); - User.auth.clearLoginAttempts = async function (uid) { - await db.delete(`loginAttempts:${uid}`); - }; + await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids); + return activeSids; +} - User.auth.resetLockout = async function (uid) { - await db.deleteAll([ - `loginAttempts:${uid}`, - `lockout:${uid}`, - ]); - }; +async function addSession(User, uid, sessionId) { + if (!(parseInt(uid, 10) > 0)) { + return; + } - User.auth.getSessions = async function (uid, curSessionId) { - await cleanExpiredSessions(uid); - const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19); - let sessions = await Promise.all(sids.map(sid => db.sessionStoreGet(sid))); - sessions = sessions.map((sessObj, idx) => { - if (sessObj && sessObj.meta) { - sessObj.meta.current = curSessionId === sids[idx]; - sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); - sessObj.meta.ip = validator.escape(String(sessObj.meta.ip)); - } - return sessObj && sessObj.meta; - }).filter(Boolean); - return sessions; - }; + const activeSids = await cleanExpiredSessions(uid); + await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId); + activeSids.push(sessionId); + await revokeSessionsAboveThreshold(User, activeSids, uid); +} - async function cleanExpiredSessions(uid) { - const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1); - if (!sids.length) { - return []; +async function revokeSessionsAboveThreshold(User, activeSids, uid) { + if (meta.config.maxUserSessions > 0 && activeSids.length > meta.config.maxUserSessions) { + const sessionsToRevoke = activeSids.slice(0, activeSids.length - meta.config.maxUserSessions); + await User.auth.revokeSession(sessionsToRevoke, uid); + } +} + +async function revokeSession(sessionIds, uid) { + sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]; + const destroySids = sids => Promise.all(sids.map(db.sessionStoreDestroy)); + + await Promise.all([ + db.sortedSetRemove(`uid:${uid}:sessions`, sessionIds), + destroySids(sessionIds), + ]); +} + +async function revokeAllSessions(User, uids, except) { + uids = Array.isArray(uids) ? uids : [uids]; + const sids = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:sessions`)); + const promises = []; + uids.forEach((uid, index) => { + const ids = sids[index].filter(id => id !== except); + if (ids.length) { + promises.push(User.auth.revokeSession(ids, uid)); } + }); + await Promise.all(promises); +} - const expiredSids = []; - const activeSids = []; - await Promise.all(sids.map(async (sid) => { - const sessionObj = await db.sessionStoreGet(sid); - const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || - !sessionObj.passport.hasOwnProperty('user') || - parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); - if (expired) { - expiredSids.push(sid); - } else { - activeSids.push(sid); - } - })); - - await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids); - return activeSids; - } +async function deleteAllSessions() { + await batch.processSortedSet('users:joindate', async (uids) => { + const sessionKeys = uids.map(uid => `uid:${uid}:sessions`); + const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1)); - User.auth.addSession = async function (uid, sessionId) { - if (!(parseInt(uid, 10) > 0)) { - return; - } + await Promise.all([ + db.deleteAll(sessionKeys), + ...sids.map(sid => db.sessionStoreDestroy(sid)), + ]); + }, { batch: 1000 }); +} + +module.exports = function (User) { + User.auth = {}; + + User.auth.logAttempt = logAttempt; - const activeSids = await cleanExpiredSessions(uid); - await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId); - await revokeSessionsAboveThreshold(activeSids.push(sessionId), uid); + User.auth.getFeedToken = async function (uid) { + return await getFeedToken(User, uid); }; - async function revokeSessionsAboveThreshold(activeSids, uid) { - if (meta.config.maxUserSessions > 0 && activeSids.length > meta.config.maxUserSessions) { - const sessionsToRevoke = activeSids.slice(0, activeSids.length - meta.config.maxUserSessions); - await User.auth.revokeSession(sessionsToRevoke, uid); - } - } + User.auth.clearLoginAttempts = clearLoginAttempts; - User.auth.revokeSession = async function (sessionIds, uid) { - sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]; - const destroySids = sids => Promise.all(sids.map(db.sessionStoreDestroy)); + User.auth.resetLockout = resetLockout; - await Promise.all([ - db.sortedSetRemove(`uid:${uid}:sessions`, sessionIds), - destroySids(sessionIds), - ]); - }; + User.auth.getSessions = getSessions; - User.auth.revokeAllSessions = async function (uids, except) { - uids = Array.isArray(uids) ? uids : [uids]; - const sids = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:sessions`)); - const promises = []; - uids.forEach((uid, index) => { - const ids = sids[index].filter(id => id !== except); - if (ids.length) { - promises.push(User.auth.revokeSession(ids, uid)); - } - }); - await Promise.all(promises); + User.auth.addSession = async function (uid, sessionId) { + await addSession(User, uid, sessionId); }; - User.auth.deleteAllSessions = async function () { - await batch.processSortedSet('users:joindate', async (uids) => { - const sessionKeys = uids.map(uid => `uid:${uid}:sessions`); - const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1)); + User.auth.revokeSession = revokeSession; - await Promise.all([ - db.deleteAll(sessionKeys), - ...sids.map(sid => db.sessionStoreDestroy(sid)), - ]); - }, { batch: 1000 }); + User.auth.revokeAllSessions = async function (uids, except) { + await revokeAllSessions(User, uids, except); }; + + User.auth.deleteAllSessions = deleteAllSessions; };