let UserSessionsManager const Settings = require('settings-sharelatex') const logger = require('logger-sharelatex') const Async = require('async') const _ = require('underscore') const UserSessionsRedis = require('./UserSessionsRedis') const rclient = UserSessionsRedis.client() module.exports = UserSessionsManager = { // mimic the key used by the express sessions _sessionKey(sessionId) { return `sess:${sessionId}` }, trackSession(user, sessionId, callback) { if (!user) { logger.log({ sessionId }, 'no user to track, returning') return callback(null) } if (!sessionId) { logger.log({ user_id: user._id }, 'no sessionId to track, returning') return callback(null) } logger.log({ user_id: user._id, sessionId }, 'onLogin handler') const sessionSetKey = UserSessionsRedis.sessionSetKey(user) const value = UserSessionsManager._sessionKey(sessionId) rclient .multi() .sadd(sessionSetKey, value) .pexpire(sessionSetKey, `${Settings.cookieSessionLength}`) // in milliseconds .exec(function(err, response) { if (err) { logger.warn( { err, user_id: user._id, sessionSetKey }, 'error while adding session key to UserSessions set' ) return callback(err) } UserSessionsManager._checkSessions(user, function() {}) callback() }) }, untrackSession(user, sessionId, callback) { if (!callback) { callback = function() {} } if (!user) { logger.log({ sessionId }, 'no user to untrack, returning') return callback(null) } if (!sessionId) { logger.log({ user_id: user._id }, 'no sessionId to untrack, returning') return callback(null) } logger.log({ user_id: user._id, sessionId }, 'onLogout handler') const sessionSetKey = UserSessionsRedis.sessionSetKey(user) const value = UserSessionsManager._sessionKey(sessionId) rclient .multi() .srem(sessionSetKey, value) .pexpire(sessionSetKey, `${Settings.cookieSessionLength}`) // in milliseconds .exec(function(err, response) { if (err) { logger.warn( { err, user_id: user._id, sessionSetKey }, 'error while removing session key from UserSessions set' ) return callback(err) } UserSessionsManager._checkSessions(user, function() {}) callback() }) }, getAllUserSessions(user, exclude, callback) { exclude = _.map(exclude, UserSessionsManager._sessionKey) const sessionSetKey = UserSessionsRedis.sessionSetKey(user) rclient.smembers(sessionSetKey, function(err, sessionKeys) { if (err) { logger.warn( { user_id: user._id }, 'error getting all session keys for user from redis' ) return callback(err) } sessionKeys = _.filter(sessionKeys, k => !_.contains(exclude, k)) if (sessionKeys.length === 0) { logger.log({ user_id: user._id }, 'no other sessions found, returning') return callback(null, []) } Async.mapSeries(sessionKeys, (k, cb) => rclient.get(k, cb), function( err, sessions ) { if (err) { logger.warn( { user_id: user._id }, 'error getting all sessions for user from redis' ) return callback(err) } const result = [] for (let session of Array.from(sessions)) { if (!session) { continue } session = JSON.parse(session) let sessionUser = session.passport && session.passport.user if (!sessionUser) { sessionUser = session.user } result.push({ ip_address: sessionUser.ip_address, session_created: sessionUser.session_created }) } callback(null, result) }) }) }, revokeAllUserSessions(user, retain, callback) { if (!retain) { retain = [] } retain = retain.map(i => UserSessionsManager._sessionKey(i)) if (!user) { logger.log({}, 'no user to revoke sessions for, returning') return callback(null) } logger.log({ user_id: user._id }, 'revoking all existing sessions for user') const sessionSetKey = UserSessionsRedis.sessionSetKey(user) rclient.smembers(sessionSetKey, function(err, sessionKeys) { if (err) { logger.warn( { err, user_id: user._id, sessionSetKey }, 'error getting contents of UserSessions set' ) return callback(err) } const keysToDelete = _.filter( sessionKeys, k => !Array.from(retain).includes(k) ) if (keysToDelete.length === 0) { logger.log( { user_id: user._id }, 'no sessions in UserSessions set to delete, returning' ) return callback(null) } logger.log( { user_id: user._id, count: keysToDelete.length }, 'deleting sessions for user' ) const deletions = keysToDelete.map(k => cb => rclient.del(k, cb)) Async.series(deletions, function(err, _result) { if (err) { logger.warn( { err, user_id: user._id, sessionSetKey }, 'errror revoking all sessions for user' ) return callback(err) } rclient.srem(sessionSetKey, keysToDelete, function(err) { if (err) { logger.warn( { err, user_id: user._id, sessionSetKey }, 'error removing session set for user' ) return callback(err) } callback(null) }) }) }) }, touch(user, callback) { if (!user) { logger.log({}, 'no user to touch sessions for, returning') return callback(null) } const sessionSetKey = UserSessionsRedis.sessionSetKey(user) rclient.pexpire( sessionSetKey, `${Settings.cookieSessionLength}`, // in milliseconds function(err, response) { if (err) { logger.warn( { err, user_id: user._id }, 'error while updating ttl on UserSessions set' ) return callback(err) } callback(null) } ) }, _checkSessions(user, callback) { if (!user) { logger.log({}, 'no user, returning') return callback(null) } logger.log({ user_id: user._id }, 'checking sessions for user') const sessionSetKey = UserSessionsRedis.sessionSetKey(user) rclient.smembers(sessionSetKey, function(err, sessionKeys) { if (err) { logger.warn( { err, user_id: user._id, sessionSetKey }, 'error getting contents of UserSessions set' ) return callback(err) } logger.log( { user_id: user._id, count: sessionKeys.length }, 'checking sessions for user' ) Async.series( sessionKeys.map(key => next => rclient.get(key, function(err, val) { if (err) { return next(err) } if (!val) { logger.log( { user_id: user._id, key }, '>> removing key from UserSessions set' ) rclient.srem(sessionSetKey, key, function(err, result) { return next(err) }) } else { next() } }) ), function(err, results) { logger.log({ user_id: user._id }, 'done checking sessions for user') callback(err) } ) }) } }