Просмотр исходного кода

Merge branch 'master' of https://github.com/sharelatex/web-sharelatex

Henry Oswald 10 лет назад
Родитель
Сommit
92e3101d81

+ 8 - 0
services/web/app/coffee/Features/User/UserController.coffee

@@ -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

+ 12 - 0
services/web/app/coffee/Features/User/UserPagesController.coffee

@@ -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

+ 31 - 2
services/web/app/coffee/Features/User/UserSessionsManager.coffee

@@ -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"

+ 3 - 0
services/web/app/coffee/router.coffee

@@ -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 - 0
services/web/app/views/user/sessions.jade

@@ -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')}

+ 8 - 0
services/web/app/views/user/settings.jade

@@ -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()
 

Разница между файлами не показана из-за своего большого размера
+ 2 - 1
services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee


+ 1 - 3
services/web/public/coffee/main.coffee

@@ -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 - 0
services/web/public/coffee/main/clear-sessions.coffee

@@ -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
+	]

+ 24 - 0
services/web/test/UnitTests/coffee/User/UserControllerTests.coffee

@@ -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)->

+ 35 - 1
services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee

@@ -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", ->
 

+ 92 - 0
services/web/test/UnitTests/coffee/User/UserSessionsManagerTests.coffee

@@ -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 ->

+ 112 - 0
services/web/test/acceptance/coffee/SessionTests.coffee

@@ -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()
+			)

Некоторые файлы не были показаны из-за большого количества измененных файлов