mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge branch 'master' of https://github.com/sharelatex/web-sharelatex
This commit is contained in:
commit
92e3101d81
13 changed files with 398 additions and 7 deletions
|
@ -108,6 +108,14 @@ module.exports = UserController =
|
|||
setNewPasswordUrl: setNewPasswordUrl
|
||||
}
|
||||
|
||||
clearSessions: (req, res, next = (error) ->) ->
|
||||
metrics.inc "user.clear-sessions"
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log {user_id: user._id}, "clearing sessions for user"
|
||||
UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) ->
|
||||
return next(err) if err?
|
||||
res.sendStatus 201
|
||||
|
||||
changePassword : (req, res, next = (error) ->)->
|
||||
metrics.inc "user.password-change"
|
||||
oldPass = req.body.currentPassword
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
UserSessionsManager = require("./UserSessionsManager")
|
||||
ErrorController = require("../Errors/ErrorController")
|
||||
logger = require("logger-sharelatex")
|
||||
Settings = require("settings-sharelatex")
|
||||
|
@ -63,3 +64,14 @@ module.exports =
|
|||
user: user,
|
||||
languages: Settings.languages,
|
||||
accountSettingsTabActive: true
|
||||
|
||||
sessionsPage: (req, res, next) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log user_id: user._id, "loading sessions page"
|
||||
UserSessionsManager.getAllUserSessions user, [req.sessionID], (err, sessions) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id}, "error getting all user sessions"
|
||||
return next(err)
|
||||
res.render 'user/sessions',
|
||||
title: "sessions"
|
||||
sessions: sessions
|
||||
|
|
|
@ -55,11 +55,40 @@ module.exports = UserSessionsManager =
|
|||
UserSessionsManager._checkSessions(user, () ->)
|
||||
callback()
|
||||
|
||||
getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) ->
|
||||
exclude = _.map(exclude, UserSessionsManager._sessionKey)
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
rclient.smembers sessionSetKey, (err, sessionKeys) ->
|
||||
if err?
|
||||
logger.err 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, [])
|
||||
rclient.mget sessionKeys, (err, sessions) ->
|
||||
if err?
|
||||
logger.err {user_id: user._id}, "error getting all sessions for user from redis"
|
||||
return callback(err)
|
||||
|
||||
result = []
|
||||
for session in sessions
|
||||
if session is null
|
||||
continue
|
||||
session = JSON.parse(session)
|
||||
session_user = session?.user or session?.passport?.user
|
||||
result.push {
|
||||
ip_address: session_user.ip_address,
|
||||
session_created: session_user.session_created
|
||||
}
|
||||
|
||||
return callback(null, result)
|
||||
|
||||
revokeAllUserSessions: (user, retain, callback=(err)->) ->
|
||||
if !retain
|
||||
if !retain?
|
||||
retain = []
|
||||
retain = retain.map((i) -> UserSessionsManager._sessionKey(i))
|
||||
if !user
|
||||
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"
|
||||
|
|
|
@ -88,6 +88,9 @@ module.exports = class Router
|
|||
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
|
||||
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
|
||||
|
||||
webRouter.get '/user/sessions', AuthenticationController.requireLogin(), UserPagesController.sessionsPage
|
||||
webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
|
||||
|
||||
webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
|
||||
webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
|
||||
|
||||
|
|
49
services/web/app/views/user/sessions.jade
Normal file
49
services/web/app/views/user/sessions.jade
Normal file
|
@ -0,0 +1,49 @@
|
|||
extends ../layout
|
||||
|
||||
|
||||
block scripts
|
||||
script(type='text/javascript').
|
||||
window.otherSessions = !{JSON.stringify(sessions)}
|
||||
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
|
||||
.card.clear-user-sessions(ng-controller="ClearSessionsController", ng-cloak)
|
||||
.page-header
|
||||
h1 #{translate("your_sessions")}
|
||||
|
||||
div
|
||||
p.small
|
||||
| #{translate("clear_sessions_description")}
|
||||
|
||||
div
|
||||
div(ng-if="state.otherSessions.length == 0")
|
||||
p.text-center
|
||||
| #{translate("no_other_sessions")}
|
||||
|
||||
div(ng-if="state.success == true")
|
||||
p.text-success.text-center
|
||||
| #{translate('clear_sessions_success')}
|
||||
|
||||
div(ng-if="state.otherSessions.length != 0")
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th #{translate("ip_address")}
|
||||
th #{translate("session_created_at")}
|
||||
tr(ng-repeat="session in state.otherSessions")
|
||||
td {{session.ip_address}}
|
||||
td {{session.session_created | formatDate}}
|
||||
|
||||
p.actions
|
||||
.text-center
|
||||
button.btn.btn-lg.btn-primary(
|
||||
ng-click="clearSessions()"
|
||||
) #{translate('clear_sessions')}
|
||||
|
||||
div(ng-if="state.error == true")
|
||||
p.text-danger.error
|
||||
| #{translate('generic_something_went_wrong')}
|
|
@ -114,6 +114,14 @@ block content
|
|||
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
|
||||
|
||||
hr
|
||||
|
||||
h3
|
||||
| #{translate("sessions")}
|
||||
|
||||
div
|
||||
a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
|
||||
|
||||
hr
|
||||
|
||||
if !externalAuthenticationSystemUsed()
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,6 +2,7 @@ define [
|
|||
"main/project-list/index"
|
||||
"main/user-details"
|
||||
"main/account-settings"
|
||||
"main/clear-sessions"
|
||||
"main/account-upgrade"
|
||||
"main/plans"
|
||||
"main/group-members"
|
||||
|
@ -32,6 +33,3 @@ define [
|
|||
"__MAIN_CLIENTSIDE_INCLUDES__"
|
||||
], () ->
|
||||
angular.bootstrap(document.body, ["SharelatexApp"])
|
||||
|
||||
|
||||
|
||||
|
|
20
services/web/public/coffee/main/clear-sessions.coffee
Normal file
20
services/web/public/coffee/main/clear-sessions.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "ClearSessionsController", ["$scope", "$http", ($scope, $http) ->
|
||||
|
||||
$scope.state =
|
||||
otherSessions: window.otherSessions
|
||||
error: false
|
||||
success: false
|
||||
|
||||
$scope.clearSessions = () ->
|
||||
console.log ">> clearing all sessions"
|
||||
$http({method: 'POST', url: "/user/sessions/clear", headers: {'X-CSRF-Token': window.csrfToken}})
|
||||
.success () ->
|
||||
$scope.state.otherSessions = []
|
||||
$scope.state.error = false
|
||||
$scope.state.success = true
|
||||
.error () ->
|
||||
$scope.state.error = true
|
||||
]
|
|
@ -81,6 +81,7 @@ describe "UserController", ->
|
|||
|
||||
@res =
|
||||
send: sinon.stub()
|
||||
sendStatus: sinon.stub()
|
||||
json: sinon.stub()
|
||||
@next = sinon.stub()
|
||||
describe "deleteUser", ->
|
||||
|
@ -224,6 +225,29 @@ describe "UserController", ->
|
|||
})
|
||||
.should.equal true
|
||||
|
||||
describe 'clearSessions', ->
|
||||
|
||||
it 'should call revokeAllUserSessions', (done) ->
|
||||
@UserController.clearSessions @req, @res
|
||||
@UserSessionsManager.revokeAllUserSessions.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'send a 201 response', (done) ->
|
||||
@res.sendStatus = (status) =>
|
||||
status.should.equal 201
|
||||
done()
|
||||
@UserController.clearSessions @req, @res
|
||||
|
||||
describe 'when revokeAllUserSessions produces an error', ->
|
||||
|
||||
it 'should call next with an error', (done) ->
|
||||
@UserSessionsManager.revokeAllUserSessions.callsArgWith(2, new Error('woops'))
|
||||
next = (err) =>
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
@UserController.clearSessions @req, @res, next
|
||||
|
||||
describe "changePassword", ->
|
||||
|
||||
it "should check the old password is the current one at the moment", (done)->
|
||||
|
|
|
@ -20,6 +20,8 @@ describe "UserPagesController", ->
|
|||
findById: sinon.stub().callsArgWith(1, null, @user)
|
||||
@UserGetter =
|
||||
getUser: sinon.stub().callsArgWith(2, null, @user)
|
||||
@UserSessionsManager =
|
||||
getAllUserSessions: sinon.stub()
|
||||
@dropboxStatus = {}
|
||||
@DropboxHandler =
|
||||
getUserRegistrationStatus : sinon.stub().callsArgWith(1, null, @dropboxStatus)
|
||||
|
@ -27,11 +29,15 @@ describe "UserPagesController", ->
|
|||
notFound: sinon.stub()
|
||||
@AuthenticationController =
|
||||
getLoggedInUserId: sinon.stub().returns(@user._id)
|
||||
getSessionUser: sinon.stub().returns(@user)
|
||||
@UserPagesController = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex": log:->
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
err:->
|
||||
"./UserLocator": @UserLocator
|
||||
"./UserGetter": @UserGetter
|
||||
"./UserSessionsManager": @UserSessionsManager
|
||||
"../Errors/ErrorController": @ErrorController
|
||||
'../Dropbox/DropboxHandler': @DropboxHandler
|
||||
'../Authentication/AuthenticationController': @AuthenticationController
|
||||
|
@ -100,6 +106,34 @@ describe "UserPagesController", ->
|
|||
done()
|
||||
@UserPagesController.loginPage @req, @res
|
||||
|
||||
describe 'sessionsPage', ->
|
||||
|
||||
beforeEach ->
|
||||
@UserSessionsManager.getAllUserSessions.callsArgWith(2, null, [])
|
||||
|
||||
it 'should render user/sessions', (done) ->
|
||||
@res.render = (page)->
|
||||
page.should.equal "user/sessions"
|
||||
done()
|
||||
@UserPagesController.sessionsPage @req, @res
|
||||
|
||||
it 'should have called getAllUserSessions', (done) ->
|
||||
@res.render = (page) =>
|
||||
@UserSessionsManager.getAllUserSessions.callCount.should.equal 1
|
||||
done()
|
||||
@UserPagesController.sessionsPage @req, @res
|
||||
|
||||
describe 'when getAllUserSessions produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@UserSessionsManager.getAllUserSessions.callsArgWith(2, new Error('woops'))
|
||||
|
||||
it 'should call next with an error', (done) ->
|
||||
@next = (err) =>
|
||||
assert(err != null)
|
||||
assert(err instanceof Error)
|
||||
done()
|
||||
@UserPagesController.sessionsPage @req, @res, @next
|
||||
|
||||
describe "settingsPage", ->
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ describe 'UserSessionsManager', ->
|
|||
sadd: sinon.stub()
|
||||
srem: sinon.stub()
|
||||
smembers: sinon.stub()
|
||||
mget: sinon.stub()
|
||||
expire: sinon.stub()
|
||||
@rclient.multi.returns(@rclient)
|
||||
@rclient.get.returns(@rclient)
|
||||
|
@ -404,6 +405,97 @@ describe 'UserSessionsManager', ->
|
|||
@rclient.expire.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'getAllUserSessions', ->
|
||||
|
||||
beforeEach ->
|
||||
@sessionKeys = ['sess:one', 'sess:two', 'sess:three']
|
||||
@sessions = [
|
||||
'{"user": {"ip_address": "a", "session_created": "b"}}',
|
||||
'{"passport": {"user": {"ip_address": "c", "session_created": "d"}}}'
|
||||
]
|
||||
@exclude = ['two']
|
||||
@rclient.smembers.callsArgWith(1, null, @sessionKeys)
|
||||
@rclient.mget.callsArgWith(1, null, @sessions)
|
||||
@call = (callback) =>
|
||||
@UserSessionsManager.getAllUserSessions @user, @exclude, callback
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, sessions) =>
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it 'should get sessions', (done) ->
|
||||
@call (err, sessions) =>
|
||||
expect(sessions).to.deep.equal [
|
||||
{ ip_address: 'a', session_created: 'b' },
|
||||
{ ip_address: 'c', session_created: 'd' }
|
||||
]
|
||||
done()
|
||||
|
||||
it 'should have called rclient.smembers', (done) ->
|
||||
@call (err, sessions) =>
|
||||
@rclient.smembers.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should have called rclient.mget', (done) ->
|
||||
@call (err, sessions) =>
|
||||
@rclient.mget.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
describe 'when there are no other sessions', ->
|
||||
|
||||
beforeEach ->
|
||||
@sessionKeys = ['sess:two']
|
||||
@rclient.smembers.callsArgWith(1, null, @sessionKeys)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, sessions) =>
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it 'should produce an empty list of sessions', (done) ->
|
||||
@call (err, sessions) =>
|
||||
expect(sessions).to.deep.equal []
|
||||
done()
|
||||
|
||||
it 'should have called rclient.smembers', (done) ->
|
||||
@call (err, sessions) =>
|
||||
@rclient.smembers.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should not have called rclient.mget', (done) ->
|
||||
@call (err, sessions) =>
|
||||
@rclient.mget.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'when smembers produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@rclient.smembers.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sessions) =>
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should not have called rclient.mget', (done) ->
|
||||
@call (err, sessions) =>
|
||||
@rclient.mget.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'when mget produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@rclient.mget.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sessions) =>
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
|
||||
describe '_checkSessions', ->
|
||||
|
||||
beforeEach ->
|
||||
|
|
|
@ -251,3 +251,115 @@ describe "Sessions", ->
|
|||
throw err
|
||||
done()
|
||||
)
|
||||
|
||||
describe 'three sessions, sessions page', ->
|
||||
|
||||
before ->
|
||||
# set up second session for this user
|
||||
@user2 = new User()
|
||||
@user2.email = @user1.email
|
||||
@user2.password = @user1.password
|
||||
@user3 = new User()
|
||||
@user3.email = @user1.email
|
||||
@user3.password = @user1.password
|
||||
|
||||
|
||||
it "should allow the user to erase the other two sessions", (done) ->
|
||||
async.series(
|
||||
[
|
||||
(next) =>
|
||||
redis.clearUserSessions @user1, next
|
||||
|
||||
# login, should add session to set
|
||||
, (next) =>
|
||||
@user1.login (err) ->
|
||||
next(err)
|
||||
|
||||
, (next) =>
|
||||
redis.getUserSessions @user1, (err, sessions) =>
|
||||
expect(sessions.length).to.equal 1
|
||||
expect(sessions[0].slice(0, 5)).to.equal 'sess:'
|
||||
next()
|
||||
|
||||
# login again, should add the second session to set
|
||||
, (next) =>
|
||||
@user2.login (err) ->
|
||||
next(err)
|
||||
|
||||
, (next) =>
|
||||
redis.getUserSessions @user1, (err, sessions) =>
|
||||
expect(sessions.length).to.equal 2
|
||||
expect(sessions[0].slice(0, 5)).to.equal 'sess:'
|
||||
expect(sessions[1].slice(0, 5)).to.equal 'sess:'
|
||||
next()
|
||||
|
||||
# login third session, should add the second session to set
|
||||
, (next) =>
|
||||
@user3.login (err) ->
|
||||
next(err)
|
||||
|
||||
, (next) =>
|
||||
redis.getUserSessions @user1, (err, sessions) =>
|
||||
expect(sessions.length).to.equal 3
|
||||
expect(sessions[0].slice(0, 5)).to.equal 'sess:'
|
||||
expect(sessions[1].slice(0, 5)).to.equal 'sess:'
|
||||
next()
|
||||
|
||||
# check the sessions page
|
||||
, (next) =>
|
||||
@user2.request.get {
|
||||
uri: '/user/sessions'
|
||||
}, (err, response, body) =>
|
||||
expect(err).to.be.oneOf [null, undefined]
|
||||
expect(response.statusCode).to.equal 200
|
||||
next()
|
||||
|
||||
# clear sessions from second session, should erase two of the three sessions
|
||||
, (next) =>
|
||||
@user2.getCsrfToken (err) =>
|
||||
expect(err).to.be.oneOf [null, undefined]
|
||||
@user2.request.post {
|
||||
uri: '/user/sessions/clear'
|
||||
}, (err) ->
|
||||
next(err)
|
||||
|
||||
, (next) =>
|
||||
redis.getUserSessions @user2, (err, sessions) =>
|
||||
expect(sessions.length).to.equal 1
|
||||
next()
|
||||
|
||||
# users one and three should not be able to access settings page
|
||||
, (next) =>
|
||||
@user1.getUserSettingsPage (err, statusCode) =>
|
||||
expect(err).to.equal null
|
||||
expect(statusCode).to.equal 302
|
||||
next()
|
||||
|
||||
, (next) =>
|
||||
@user3.getUserSettingsPage (err, statusCode) =>
|
||||
expect(err).to.equal null
|
||||
expect(statusCode).to.equal 302
|
||||
next()
|
||||
|
||||
# user two should still be logged in, and able to access settings page
|
||||
, (next) =>
|
||||
@user2.getUserSettingsPage (err, statusCode) =>
|
||||
expect(err).to.equal null
|
||||
expect(statusCode).to.equal 200
|
||||
next()
|
||||
|
||||
# logout second session, should remove last session from set
|
||||
, (next) =>
|
||||
@user2.logout (err) ->
|
||||
next(err)
|
||||
|
||||
, (next) =>
|
||||
redis.getUserSessions @user1, (err, sessions) =>
|
||||
expect(sessions.length).to.equal 0
|
||||
next()
|
||||
|
||||
], (err, result) =>
|
||||
if err
|
||||
throw err
|
||||
done()
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue