From d7dafbda2f930ad992caeece241b4446a365af42 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:42:20 +0300 Subject: [PATCH 1/9] refactor(user/auth): extract logAttempt function --- src/user/auth.js | 52 +++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index 0adf589967..d46ca12e1d 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -8,34 +8,36 @@ const events = require('../events'); const batch = require('../batch'); const utils = require('../utils'); +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]]'); + } + 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]]'); +} + 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, - }); - throw new Error('[[error:account-locked]]'); - }; + User.auth.logAttempt = logAttempt; User.auth.getFeedToken = async function (uid) { if (!(parseInt(uid, 10) > 0)) { From b25d5b8ef02ceb72cb600bcb416b2fabba9fa438 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:42:41 +0300 Subject: [PATCH 2/9] refactor(user/auth): extract getFeedToken function --- src/user/auth.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index d46ca12e1d..ef38c12857 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -34,21 +34,25 @@ async function logAttempt(uid, 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; +} + module.exports = function (User) { User.auth = {}; User.auth.logAttempt = logAttempt; User.auth.getFeedToken = async function (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; + return await getFeedToken(User, uid); }; User.auth.clearLoginAttempts = async function (uid) { From dc4513ec4e7d60c662ded10a0ede18608124f129 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:43:18 +0300 Subject: [PATCH 3/9] refactor(user/auth): extract clearLoginAttempts function --- src/user/auth.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index ef38c12857..c7e55a20a9 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -46,6 +46,10 @@ async function getFeedToken(User, uid) { return token; } +async function clearLoginAttempts(uid) { + await db.delete(`loginAttempts:${uid}`); +} + module.exports = function (User) { User.auth = {}; @@ -55,9 +59,7 @@ module.exports = function (User) { return await getFeedToken(User, uid); }; - User.auth.clearLoginAttempts = async function (uid) { - await db.delete(`loginAttempts:${uid}`); - }; + User.auth.clearLoginAttempts = clearLoginAttempts; User.auth.resetLockout = async function (uid) { await db.deleteAll([ From 331b60bb8420b7126f7a442db9a292ee24b08f49 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:43:42 +0300 Subject: [PATCH 4/9] refactor(user/auth): extract resetLockout function --- src/user/auth.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index c7e55a20a9..4fd9ca01d7 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -50,6 +50,13 @@ async function clearLoginAttempts(uid) { await db.delete(`loginAttempts:${uid}`); } +async function resetLockout(uid) { + await db.deleteAll([ + `loginAttempts:${uid}`, + `lockout:${uid}`, + ]); +} + module.exports = function (User) { User.auth = {}; @@ -61,12 +68,7 @@ module.exports = function (User) { User.auth.clearLoginAttempts = clearLoginAttempts; - User.auth.resetLockout = async function (uid) { - await db.deleteAll([ - `loginAttempts:${uid}`, - `lockout:${uid}`, - ]); - }; + User.auth.resetLockout = resetLockout; User.auth.getSessions = async function (uid, curSessionId) { await cleanExpiredSessions(uid); From 1c8eea595ce131cc1e5dbfc27ea09d79f7d4021a Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:44:16 +0300 Subject: [PATCH 5/9] refactor(user/auth): extract getSessions and cleanExpiredSessions functions --- src/user/auth.js | 54 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index 4fd9ca01d7..bcf37d7912 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -57,6 +57,45 @@ async function resetLockout(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)); + } + 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); + } + })); + + await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids); + return activeSids; +} + module.exports = function (User) { User.auth = {}; @@ -70,20 +109,7 @@ module.exports = function (User) { User.auth.resetLockout = resetLockout; - 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; - }; + User.auth.getSessions = getSessions; async function cleanExpiredSessions(uid) { const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1); From 45274055bcd96ababc03ea0c9e48b0a55a4bd850 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:45:21 +0300 Subject: [PATCH 6/9] refactor(user/auth): extract addSession and revokeSessionsAboveThreshold functions --- src/user/auth.js | 57 ++++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index bcf37d7912..efe74c55fd 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -96,6 +96,24 @@ async function cleanExpiredSessions(uid) { return activeSids; } +async function addSession(User, uid, sessionId) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + + const activeSids = await cleanExpiredSessions(uid); + await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId); + activeSids.push(sessionId); + await revokeSessionsAboveThreshold(User, activeSids, uid); +} + +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); + } +} + module.exports = function (User) { User.auth = {}; @@ -111,47 +129,10 @@ module.exports = function (User) { User.auth.getSessions = getSessions; - 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); - } - })); - - await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids); - return activeSids; - } - User.auth.addSession = async function (uid, sessionId) { - if (!(parseInt(uid, 10) > 0)) { - return; - } - - const activeSids = await cleanExpiredSessions(uid); - await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId); - await revokeSessionsAboveThreshold(activeSids.push(sessionId), uid); + await addSession(User, uid, sessionId); }; - 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.revokeSession = async function (sessionIds, uid) { sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]; const destroySids = sids => Promise.all(sids.map(db.sessionStoreDestroy)); From 9cb8e5c691f173d6cd7d8b523122e300f3b5b212 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:45:49 +0300 Subject: [PATCH 7/9] refactor(user/auth): extract revokeSession function --- src/user/auth.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index efe74c55fd..41b0e18581 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -114,6 +114,16 @@ async function revokeSessionsAboveThreshold(User, activeSids, 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), + ]); +} + module.exports = function (User) { User.auth = {}; @@ -133,15 +143,7 @@ module.exports = function (User) { await addSession(User, uid, sessionId); }; - User.auth.revokeSession = async function (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), - ]); - }; + User.auth.revokeSession = revokeSession; User.auth.revokeAllSessions = async function (uids, except) { uids = Array.isArray(uids) ? uids : [uids]; From 87e0f1a63f9b940d54716e9f4521c5c21b776b12 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:46:37 +0300 Subject: [PATCH 8/9] refactor(user/auth): extract revokeAllSessions function --- src/user/auth.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index 41b0e18581..9dcf255b16 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -124,6 +124,19 @@ async function revokeSession(sessionIds, uid) { ]); } +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); +} + module.exports = function (User) { User.auth = {}; @@ -146,16 +159,7 @@ module.exports = function (User) { User.auth.revokeSession = revokeSession; 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); + await revokeAllSessions(User, uids, except); }; User.auth.deleteAllSessions = async function () { From e204930f929329c60609f907a62b5a3d96640187 Mon Sep 17 00:00:00 2001 From: Hazem Elsayed Date: Sat, 6 Sep 2025 13:47:18 +0300 Subject: [PATCH 9/9] refactor(user/auth): extract deleteAllSessions function --- src/user/auth.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/user/auth.js b/src/user/auth.js index 9dcf255b16..de2882d187 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -137,6 +137,18 @@ async function revokeAllSessions(User, uids, except) { await Promise.all(promises); } +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)); + + await Promise.all([ + db.deleteAll(sessionKeys), + ...sids.map(sid => db.sessionStoreDestroy(sid)), + ]); + }, { batch: 1000 }); +} + module.exports = function (User) { User.auth = {}; @@ -162,15 +174,5 @@ module.exports = function (User) { await revokeAllSessions(User, uids, except); }; - 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)); - - await Promise.all([ - db.deleteAll(sessionKeys), - ...sids.map(sid => db.sessionStoreDestroy(sid)), - ]); - }, { batch: 1000 }); - }; + User.auth.deleteAllSessions = deleteAllSessions; };