From 8089bb55a48dbf7ad35085f0d0b2b8b01bc13b06 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Nov 2016 14:24:36 +0000 Subject: [PATCH 01/15] use session for the post-login redirect, remove `redir` query string. --- .../AuthenticationController.coffee | 34 +++++--- .../Features/User/UserPagesController.coffee | 2 - services/web/app/views/user/login.jade | 1 - .../AuthenticationControllerTests.coffee | 59 +++++++++----- .../User/UserPagesControllerTests.coffee | 16 ---- .../coffee/ProjectInviteTests.coffee | 77 +++++++------------ 6 files changed, 92 insertions(+), 97 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 3c19f0de25..765065823b 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -62,17 +62,18 @@ module.exports = AuthenticationController = if err? return next(err) if user # `user` is either a user object or false + redir = AuthenticationController._getRedirectFromSession(req) || "/project" AuthenticationController.afterLoginSessionSetup req, user, (err) -> if err? return next(err) - res.json {redir: req._redir} + AuthenticationController._clearRedirectFromSession(req) + res.json {redir: redir} else res.json message: info )(req, res, next) doPassportLogin: (req, username, password, done) -> email = username.toLowerCase() - redir = Url.parse(req?.body?.redir or "/project").path LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> return done(err) if err? if !isAllowed @@ -90,7 +91,6 @@ module.exports = AuthenticationController = req.session.justLoggedIn = true # capture the request ip for use when creating the session user._login_req_ip = req.ip - req._redir = redir return done(null, user) else AuthenticationController._recordFailedLogin() @@ -127,7 +127,7 @@ module.exports = AuthenticationController = requireLogin: () -> doRequest = (req, res, next = (error) ->) -> if !AuthenticationController.isUserLoggedIn(req) - AuthenticationController._redirectToLoginOrRegisterPage(req, res) + AuthenticationController._redirectToLoginOrRegisterPage(req, res, next) else req.user = AuthenticationController.getSessionUser(req) next() @@ -156,22 +156,22 @@ module.exports = AuthenticationController = logger.err user:user, pass:pass, "invalid login details" return isValid - _redirectToLoginOrRegisterPage: (req, res)-> + _redirectToLoginOrRegisterPage: (req, res, next)-> if req.query.zipUrl? or req.query.project_name? - return AuthenticationController._redirectToRegisterPage(req, res) + return AuthenticationController._redirectToRegisterPage(req, res, next) else - AuthenticationController._redirectToLoginPage(req, res) + AuthenticationController._redirectToLoginPage(req, res, next) - _redirectToLoginPage: (req, res) -> + _redirectToLoginPage: (req, res, next) -> logger.log url: req.url, "user not logged in so redirecting to login page" - req.query.redir = req.path + AuthenticationController._setRedirectInSession(req) url = "/login?#{querystring.stringify(req.query)}" res.redirect url Metrics.inc "security.login-redirect" - _redirectToRegisterPage: (req, res) -> + _redirectToRegisterPage: (req, res, next) -> logger.log url: req.url, "user not logged in so redirecting to register page" - req.query.redir = req.path + AuthenticationController._setRedirectInSession(req) url = "/register?#{querystring.stringify(req.query)}" res.redirect url Metrics.inc "security.login-redirect" @@ -188,3 +188,15 @@ module.exports = AuthenticationController = _recordFailedLogin: (callback = (error) ->) -> Metrics.inc "user.login.failed" callback() + + _setRedirectInSession: (req) -> + target = if Object.keys(req.query) then "#{req.path}?#{querystring.stringify(req.query)}" else req.path + if req.session? + req.session.postLoginRedirect = target + + _getRedirectFromSession: (req) -> + return req?.session?.postLoginRedirect || null + + _clearRedirectFromSession: (req) -> + if req.session? + delete req.session.postLoginRedirect diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 76d88803a7..00494d71e5 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -20,7 +20,6 @@ module.exports = res.render 'user/register', title: 'register' - redir: req.query.redir sharedProjectData: sharedProjectData newTemplateData: newTemplateData new_email:req.query.new_email || "" @@ -51,7 +50,6 @@ module.exports = loginPage : (req, res)-> res.render 'user/login', title: 'login', - redir: req.query.redir, email: req.query.email settingsPage : (req, res, next)-> diff --git a/services/web/app/views/user/login.jade b/services/web/app/views/user/login.jade index a6587782bf..8339f27189 100644 --- a/services/web/app/views/user/login.jade +++ b/services/web/app/views/user/login.jade @@ -10,7 +10,6 @@ block content h1 #{translate("log_in")} form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak) input(name='_csrf', type='hidden', value=csrfToken) - input(name='redir', type='hidden', value=redir) form-messages(for="loginForm") .form-group input.form-control( diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index e5a4486d4a..7dd2617e66 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -91,7 +91,10 @@ describe "AuthenticationController", -> @info = null @req.login = sinon.stub().callsArgWith(1, null) @res.json = sinon.stub() - @req.session = @session = {passport: {user: @user}} + @req.session = @session = { + passport: {user: @user}, + postLoginRedirect: "/path/to/redir/to" + } @req.session.destroy = sinon.stub().callsArgWith(0, null) @req.session.save = sinon.stub().callsArgWith(0, null) @req.sessionStore = {generate: sinon.stub()} @@ -114,11 +117,11 @@ describe "AuthenticationController", -> describe 'when authenticate produces a user', -> beforeEach -> - @req._redir = 'some_redirect' + @req.session.postLoginRedirect = 'some_redirect' @passport.authenticate.callsArgWith(1, null, @user, @info) afterEach -> - delete @req._redir + delete @req.session.postLoginRedirect it 'should call req.login', () -> @AuthenticationController.passportLogin @req, @res, @next @@ -128,7 +131,7 @@ describe "AuthenticationController", -> it 'should send a json response with redirect', () -> @AuthenticationController.passportLogin @req, @res, @next @res.json.callCount.should.equal 1 - @res.json.calledWith({redir: @req._redir}).should.equal true + @res.json.calledWith({redir: 'some_redirect'}).should.equal true describe 'when session.save produces an error', () -> beforeEach -> @@ -230,7 +233,8 @@ describe "AuthenticationController", -> @req.body = email: @email password: @password - redir: @redir = "/path/to/redir/to" + session: + postLoginRedirect: "/path/to/redir/to" @cb = sinon.stub() describe "when the users rate limit", -> @@ -313,17 +317,6 @@ describe "AuthenticationController", -> .calledWith(email: @email.toLowerCase(), "failed log in") .should.equal true - describe "with a URL to a different domain", -> - beforeEach -> - @LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) - @req.body.redir = "http://www.facebook.com/test" - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) - @cb = sinon.stub() - @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) - - it "should only redirect to the local path", -> - expect(@req._redir).to.equal "/test" - describe "getLoggedInUserId", -> beforeEach -> @@ -488,8 +481,8 @@ describe "AuthenticationController", -> @AuthenticationController._redirectToRegisterPage(@req, @res) it "should redirect to the register page with a query string attached", -> - @res.redirectedTo - .should.equal "/register?extra_query=foo&redir=%2Ftarget%2Furl" + @req.session.postLoginRedirect.should.equal '/target/url?extra_query=foo' + @res.redirectedTo.should.equal "/register?extra_query=foo" it "should log out a message", -> @logger.log @@ -504,7 +497,8 @@ describe "AuthenticationController", -> @AuthenticationController._redirectToLoginPage(@req, @res) it "should redirect to the register page with a query string attached", -> - @res.redirectedTo.should.equal "/login?extra_query=foo&redir=%2Ftarget%2Furl" + @req.session.postLoginRedirect.should.equal '/target/url?extra_query=foo' + @res.redirectedTo.should.equal "/login?extra_query=foo" describe "_recordSuccessfulLogin", -> @@ -535,3 +529,30 @@ describe "AuthenticationController", -> it "should call the callback", -> @callback.called.should.equal true + + + describe '_setRedirectInSession', -> + beforeEach -> + @req = {session: {}} + @req.path = "/somewhere" + @req.query = {one: "1"} + + it 'should set redirect property on session', -> + @AuthenticationController._setRedirectInSession(@req) + expect(@req.session.postLoginRedirect).to.equal "/somewhere?one=1" + + describe '_getRedirectFromSession', -> + beforeEach -> + @req = {session: {postLoginRedirect: "/a?b=c"}} + + it 'should get redirect property from session', -> + expect(@AuthenticationController._getRedirectFromSession(@req)).to.equal "/a?b=c" + + describe '_clearRedirectFromSession', -> + beforeEach -> + @req = {session: {postLoginRedirect: "/a?b=c"}} + + it 'should remove the redirect property from session', -> + @AuthenticationController._clearRedirectFromSession(@req) + expect(@req.session.postLoginRedirect).to.equal undefined + diff --git a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee index bb9fa22a15..24ca3eeeda 100644 --- a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee @@ -56,14 +56,6 @@ describe "UserPagesController", -> done() @UserPagesController.registerPage @req, @res - it "should set the redirect", (done)-> - redirect = "/go/here/please" - @req.query.redir = redirect - @res.render = (page, opts)=> - opts.redir.should.equal redirect - done() - @UserPagesController.registerPage @req, @res - it "should set sharedProjectData", (done)-> @req.query.project_name = "myProject" @req.query.user_first_name = "user_first_name_here" @@ -98,14 +90,6 @@ describe "UserPagesController", -> done() @UserPagesController.loginPage @req, @res - it "should set the redirect", (done)-> - redirect = "/go/here/please" - @req.query.redir = redirect - @res.render = (page, opts)=> - opts.redir.should.equal redirect - done() - @UserPagesController.loginPage @req, @res - describe 'sessionsPage', -> beforeEach -> diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index c322317b2f..3068892a68 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -60,7 +60,7 @@ tryAcceptInvite = (user, invite, callback=(err, response, body)->) -> token: invite.token }, callback -tryRegisterUser = (user, email, redir, callback=(err, response, body)->) -> +tryRegisterUser = (user, email, callback=(err, response, body)->) -> user.getCsrfToken (error) => return callback(error) if error? user.request.post { @@ -68,7 +68,6 @@ tryRegisterUser = (user, email, redir, callback=(err, response, body)->) -> json: email: email password: "some_weird_password" - redir: redir }, callback tryFollowLoginLink = (user, loginLink, callback=(err, response, body)->) -> @@ -76,7 +75,7 @@ tryFollowLoginLink = (user, loginLink, callback=(err, response, body)->) -> return callback(error) if error? user.request.get loginLink, callback -tryLoginUser = (user, redir, callback=(err, response, body)->) -> +tryLoginUser = (user, callback=(err, response, body)->) -> user.getCsrfToken (error) => return callback(error) if error? user.request.post { @@ -84,7 +83,6 @@ tryLoginUser = (user, redir, callback=(err, response, body)->) -> json: email: user.email password: user.password - redir: redir }, callback tryGetInviteList = (user, projectId, callback=(err, response, body)->) -> @@ -143,35 +141,34 @@ expectInviteRedirectToRegister = (user, link, callback=(err,result)->) -> tryFollowInviteLink user, link, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$") + expect(response.headers.location).to.match new RegExp("^/register.*$") # follow redirect to register page and extract the redirectUrl from form user.request.get response.headers.location, (err, response, body) -> - redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] - loginUrl = body.match(/href="([^"]*)">\s*Login here/m)?[1] - expect(redirectUrl).to.not.be.oneOf [null, undefined] - expect(loginUrl).to.not.be.oneOf [null, undefined] - callback(null, redirectUrl, loginUrl) + # redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] + # loginUrl = body.match(/href="([^"]*)">\s*Login here/m)?[1] + # expect(redirectUrl).to.not.be.oneOf [null, undefined] + # expect(loginUrl).to.not.be.oneOf [null, undefined] + callback(null) -expectLoginPage = (user, loginLink, callback=(err, result)->) -> - tryFollowLoginLink user, loginLink, (err, response, body) -> +expectLoginPage = (user, callback=(err, result)->) -> + tryFollowLoginLink user, "/login", (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 200 expect(body).to.match new RegExp("Login - .*") - redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] - callback(null, redirectUrl) + callback(null) -expectLoginRedirectToInvite = (user, redir, link, callback=(err, result)->) -> - tryLoginUser user, redir, (err, response, body) -> +expectLoginRedirectToInvite = (user, link, callback=(err, result)->) -> + tryLoginUser user, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 200 - expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") + # expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") callback(null, null) -expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) -> - tryRegisterUser user, email, redir, (err, response, body) -> +expectRegistrationRedirectToInvite = (user, email, link, callback=(err, result)->) -> + tryRegisterUser user, email, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 200 - expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") + # expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") callback(null, null) expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) -> @@ -424,6 +421,7 @@ describe "ProjectInviteTests", -> (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) ], done + # # # # describe 'registration prompt workflow with valid token', -> it 'should redirect to the register page', (done) -> @@ -433,16 +431,14 @@ describe "ProjectInviteTests", -> it 'should allow user to accept the invite if the user registers a new account', (done) -> Async.series [ - (cb) => - expectInviteRedirectToRegister @user, @link, (err, redirectUrl) => - @_redir = redirectUrl - cb() - (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb + (cb) => expectInviteRedirectToRegister @user, @link, cb + (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @link, cb (cb) => expectInvitePage @user, @link, cb (cb) => expectAcceptInviteAndRedirect @user, @invite, cb (cb) => expectProjectAccess @user, @invite.projectId, cb ], done + # # # # describe 'registration prompt workflow with non-valid token', -> before (done)-> @@ -457,11 +453,8 @@ describe "ProjectInviteTests", -> it 'should display invalid-invite if the user registers a new account', (done) -> badLink = @link.replace(@invite.token, 'not_a_real_token') Async.series [ - (cb) => - expectInviteRedirectToRegister @user, badLink, (err, redirectUrl) => - @_redir = redirectUrl - cb() - (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb + (cb) => expectInviteRedirectToRegister @user, badLink, cb + (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", badLink, cb (cb) => expectInvalidInvitePage @user, badLink, cb (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done @@ -479,16 +472,9 @@ describe "ProjectInviteTests", -> it 'should allow the user to login to view the invite', (done) -> Async.series [ - (cb) => - expectInviteRedirectToRegister @user, @link, (err, redirectUrl, loginUrl) => - @_redir = redirectUrl - @_loginLink = loginUrl - cb() - (cb) => - expectLoginPage @user, @_loginLink, (err, redirectUrl) => - expect(@_redir).to.equal redirectUrl - cb() - (cb) => expectLoginRedirectToInvite @user, @_redir, @link, cb + (cb) => expectInviteRedirectToRegister @user, @link, cb + (cb) => expectLoginPage @user, cb + (cb) => expectLoginRedirectToInvite @user, @link, cb (cb) => expectInvitePage @user, @link, cb (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done @@ -515,15 +501,10 @@ describe "ProjectInviteTests", -> badLink = @link.replace(@invite.token, 'not_a_real_token') Async.series [ (cb) => - expectInviteRedirectToRegister @user, badLink, (err, redirectUrl, loginUrl) => - @_redir = redirectUrl - @_loginLink = loginUrl - cb() + expectInviteRedirectToRegister @user, badLink, cb (cb) => - expectLoginPage @user, @_loginLink, (err, redirectUrl) => - expect(@_redir).to.equal redirectUrl - cb() - (cb) => expectLoginRedirectToInvite @user, @_redir, badLink, cb + expectLoginPage @user, cb + (cb) => expectLoginRedirectToInvite @user, badLink, cb (cb) => expectInvalidInvitePage @user, badLink, cb (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done From 8a4352fff2e1e47478756c6498b2aaca140ad205 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Nov 2016 16:54:03 +0000 Subject: [PATCH 02/15] Set redirect when redirecting from restricted --- .../Authentication/AuthenticationController.coffee | 7 ++++--- .../Features/Authorization/AuthorizationMiddlewear.coffee | 2 +- .../Authentication/AuthenticationControllerTests.coffee | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 765065823b..7a913604d9 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -189,10 +189,11 @@ module.exports = AuthenticationController = Metrics.inc "user.login.failed" callback() - _setRedirectInSession: (req) -> - target = if Object.keys(req.query) then "#{req.path}?#{querystring.stringify(req.query)}" else req.path + _setRedirectInSession: (req, value) -> + if !value? + value = if Object.keys(req.query) > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path if req.session? - req.session.postLoginRedirect = target + req.session.postLoginRedirect = value _getRedirectFromSession: (req) -> return req?.session?.postLoginRedirect || null diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index cb0f583674..be0a85107c 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -108,5 +108,5 @@ module.exports = AuthorizationMiddlewear = logger.log {from: from}, "redirecting to login" redirect_to = "/login" if from? - redirect_to += "?redir=#{encodeURIComponent(from)}" + AuthenticationController._setRedirectInSession(req, from) res.redirect redirect_to diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 7dd2617e66..378c3bafd2 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -541,6 +541,10 @@ describe "AuthenticationController", -> @AuthenticationController._setRedirectInSession(@req) expect(@req.session.postLoginRedirect).to.equal "/somewhere?one=1" + it 'should set the supplied value', -> + @AuthenticationController._setRedirectInSession(@req, '/somewhere/specific') + expect(@req.session.postLoginRedirect).to.equal "/somewhere/specific" + describe '_getRedirectFromSession', -> beforeEach -> @req = {session: {postLoginRedirect: "/a?b=c"}} From cee3326ce3cda373114756200c3c1bbf785508ec Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Nov 2016 17:06:05 +0000 Subject: [PATCH 03/15] fix omission of 'length' --- .../Features/Authentication/AuthenticationController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 7a913604d9..59f55127f1 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -191,7 +191,7 @@ module.exports = AuthenticationController = _setRedirectInSession: (req, value) -> if !value? - value = if Object.keys(req.query) > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path + value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path if req.session? req.session.postLoginRedirect = value From 22101d030571f5886c0b1029d65b18966807c804 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 Nov 2016 11:38:13 +0000 Subject: [PATCH 04/15] If user is sent to login page with explicit redirect, obey --- .../AuthenticationController.coffee | 4 +++- .../Subscription/SubscriptionController.coffee | 5 +---- .../Features/User/UserPagesController.coffee | 5 +++++ services/web/app/views/user/register.jade | 2 +- .../AuthenticationControllerTests.coffee | 6 ++---- .../coffee/User/UserPagesControllerTests.coffee | 16 ++++++++++++++++ .../acceptance/coffee/ProjectInviteTests.coffee | 8 -------- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 59f55127f1..5a92cc0f75 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -157,7 +157,9 @@ module.exports = AuthenticationController = return isValid _redirectToLoginOrRegisterPage: (req, res, next)-> - if req.query.zipUrl? or req.query.project_name? + if (req.query.zipUrl? or + req.query.project_name? or + req.path == '/user/subscription/new') return AuthenticationController._redirectToRegisterPage(req, res, next) else AuthenticationController._redirectToLoginPage(req, res, next) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index 51b3db8f56..d864cac1d1 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -14,10 +14,7 @@ module.exports = SubscriptionController = plansPage: (req, res, next) -> plans = SubscriptionViewModelBuilder.buildViewModel() - if AuthenticationController.isUserLoggedIn(req) - baseUrl = "" - else - baseUrl = "/register?redir=" + baseUrl = "" viewName = "subscriptions/plans" if req.query.v? viewName = "#{viewName}_#{req.query.v}" diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 00494d71e5..54cfd54892 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -48,6 +48,11 @@ module.exports = token: req.query.token loginPage : (req, res)-> + # if user is being sent to /login with explicit redirect (redir=/foo), + # such as being sent from the editor to /login, then set the redirect explicitly + if req.query.redir? and !AuthenticationController._getRedirectFromSession(req)? + logger.log {redir: req.query.redir}, "setting explicit redirect from login page" + AuthenticationController._setRedirectInSession(req, req.query.redir) res.render 'user/login', title: 'login', email: req.query.email diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade index 5a1196b6a6..2a5ab707a6 100644 --- a/services/web/app/views/user/register.jade +++ b/services/web/app/views/user/register.jade @@ -12,7 +12,7 @@ block content | #{translate("join_sl_to_view_project")}. div | #{translate("if_you_are_registered")}, - a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")} + a(href="/login") #{translate("login_here")} else if newTemplateData.templateName !== undefined h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})} diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 378c3bafd2..72265eac11 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -155,10 +155,11 @@ describe "AuthenticationController", -> @AuthenticationController.passportLogin @req, @res, @next @req.login.callCount.should.equal 0 - it 'should send a json response with redirect', () -> + it 'should not send a json response with redirect', () -> @AuthenticationController.passportLogin @req, @res, @next @res.json.callCount.should.equal 1 @res.json.calledWith({message: @info}).should.equal true + expect(@res.json.lastCall.args[0].redir?).to.equal false describe 'afterLoginSessionSetup', -> @@ -269,9 +270,6 @@ describe "AuthenticationController", -> it "should set res.session.justLoggedIn", -> @req.session.justLoggedIn.should.equal true - it "should redirect the user to the specified location", -> - expect(@req._redir).to.deep.equal @redir - it "should record the successful login", -> @AuthenticationController._recordSuccessfulLogin .calledWith(@user._id) diff --git a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee index 24ca3eeeda..f0fa84176f 100644 --- a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee @@ -30,6 +30,8 @@ describe "UserPagesController", -> @AuthenticationController = getLoggedInUserId: sinon.stub().returns(@user._id) getSessionUser: sinon.stub().returns(@user) + _getRedirectFromSession: sinon.stub() + _setRedirectInSession: sinon.stub() @UserPagesController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -90,6 +92,20 @@ describe "UserPagesController", -> done() @UserPagesController.loginPage @req, @res + describe 'when an explicit redirect is set via query string', -> + + beforeEach -> + @AuthenticationController._getRedirectFromSession = sinon.stub().returns(null) + @AuthenticationController._setRedirectInSession = sinon.stub() + @req.query.redir = '/somewhere/in/particular' + + it 'should set a redirect', (done) -> + @res.render = (page) => + @AuthenticationController._setRedirectInSession.callCount.should.equal 1 + expect(@AuthenticationController._setRedirectInSession.lastCall.args[1]).to.equal @req.query.redir + done() + @UserPagesController.loginPage @req, @res + describe 'sessionsPage', -> beforeEach -> diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 3068892a68..b599578738 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -144,10 +144,6 @@ expectInviteRedirectToRegister = (user, link, callback=(err,result)->) -> expect(response.headers.location).to.match new RegExp("^/register.*$") # follow redirect to register page and extract the redirectUrl from form user.request.get response.headers.location, (err, response, body) -> - # redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] - # loginUrl = body.match(/href="([^"]*)">\s*Login here/m)?[1] - # expect(redirectUrl).to.not.be.oneOf [null, undefined] - # expect(loginUrl).to.not.be.oneOf [null, undefined] callback(null) expectLoginPage = (user, callback=(err, result)->) -> @@ -161,14 +157,12 @@ expectLoginRedirectToInvite = (user, link, callback=(err, result)->) -> tryLoginUser user, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 200 - # expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") callback(null, null) expectRegistrationRedirectToInvite = (user, email, link, callback=(err, result)->) -> tryRegisterUser user, email, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 200 - # expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") callback(null, null) expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) -> @@ -421,7 +415,6 @@ describe "ProjectInviteTests", -> (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) ], done - # # # # describe 'registration prompt workflow with valid token', -> it 'should redirect to the register page', (done) -> @@ -438,7 +431,6 @@ describe "ProjectInviteTests", -> (cb) => expectProjectAccess @user, @invite.projectId, cb ], done - # # # # describe 'registration prompt workflow with non-valid token', -> before (done)-> From 167f01857a87583b16c0e7b9f649c0ec61a7817e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 Nov 2016 14:15:01 +0000 Subject: [PATCH 05/15] Remove stray `next` params. --- .../Authentication/AuthenticationController.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 5a92cc0f75..df92fa1d7d 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -127,7 +127,7 @@ module.exports = AuthenticationController = requireLogin: () -> doRequest = (req, res, next = (error) ->) -> if !AuthenticationController.isUserLoggedIn(req) - AuthenticationController._redirectToLoginOrRegisterPage(req, res, next) + AuthenticationController._redirectToLoginOrRegisterPage(req, res) else req.user = AuthenticationController.getSessionUser(req) next() @@ -156,22 +156,22 @@ module.exports = AuthenticationController = logger.err user:user, pass:pass, "invalid login details" return isValid - _redirectToLoginOrRegisterPage: (req, res, next)-> + _redirectToLoginOrRegisterPage: (req, res)-> if (req.query.zipUrl? or req.query.project_name? or req.path == '/user/subscription/new') - return AuthenticationController._redirectToRegisterPage(req, res, next) + return AuthenticationController._redirectToRegisterPage(req, res) else - AuthenticationController._redirectToLoginPage(req, res, next) + AuthenticationController._redirectToLoginPage(req, res) - _redirectToLoginPage: (req, res, next) -> + _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" AuthenticationController._setRedirectInSession(req) url = "/login?#{querystring.stringify(req.query)}" res.redirect url Metrics.inc "security.login-redirect" - _redirectToRegisterPage: (req, res, next) -> + _redirectToRegisterPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to register page" AuthenticationController._setRedirectInSession(req) url = "/register?#{querystring.stringify(req.query)}" From 2ab5ad423072d817c95d2269a4a169b660dcd7d2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 Nov 2016 14:38:00 +0000 Subject: [PATCH 06/15] If we're updating details from a SSO source, don't allow editing on settings page. --- .../Features/User/UserPagesController.coffee | 2 + services/web/app/views/user/settings.jade | 47 +++++++++++-------- .../User/UserPagesControllerTests.coffee | 36 +++++++++++++- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 76d88803a7..31dbc44baf 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -57,11 +57,13 @@ module.exports = settingsPage : (req, res, next)-> user_id = AuthenticationController.getLoggedInUserId(req) logger.log user: user_id, "loading settings page" + shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin) UserLocator.findById user_id, (err, user)-> return next(err) if err? res.render 'user/settings', title:'account_settings' user: user, + shouldAllowEditingDetails: shouldAllowEditingDetails languages: Settings.languages, accountSettingsTabActive: true diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.jade index 14d6902899..310912cf07 100644 --- a/services/web/app/views/user/settings.jade +++ b/services/web/app/views/user/settings.jade @@ -39,25 +39,34 @@ block content label.control-label #{translate("email")} div.form-control(readonly="true") #{user.email} - .form-group - label(for='firstName').control-label #{translate("first_name")} - input.form-control( - type='text', - name='first_name', - value=user.first_name - ) - .form-group - label(for='lastName').control-label #{translate("last_name")} - input.form-control( - type='text', - name='last_name', - value=user.last_name - ) - .actions - button.btn.btn-primary( - type='submit', - ng-disabled="settingsForm.$invalid" - ) #{translate("update")} + if shouldAllowEditingDetails + .form-group + label(for='firstName').control-label #{translate("first_name")} + input.form-control( + type='text', + name='first_name', + value=user.first_name + ) + .form-group + label(for='lastName').control-label #{translate("last_name")} + input.form-control( + type='text', + name='last_name', + value=user.last_name + ) + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="settingsForm.$invalid" + ) #{translate("update")} + else + .form-group + label.control-label #{translate("first_name")} + div.form-control(readonly="true") #{user.first_name} + .form-group + label.control-label #{translate("last_name")} + div.form-control(readonly="true") #{user.last_name} + if !externalAuthenticationSystemUsed() .col-md-5.col-md-offset-1 h3 #{translate("change_password")} diff --git a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee index bb9fa22a15..041a20147f 100644 --- a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee @@ -31,7 +31,7 @@ describe "UserPagesController", -> getLoggedInUserId: sinon.stub().returns(@user._id) getSessionUser: sinon.stub().returns(@user) @UserPagesController = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings + "settings-sharelatex": @settings "logger-sharelatex": log:-> err:-> @@ -149,6 +149,40 @@ describe "UserPagesController", -> done() @UserPagesController.settingsPage @req, @res + it "should set 'shouldAllowEditingDetails' to true", (done)-> + @res.render = (page, opts)=> + opts.shouldAllowEditingDetails.should.equal true + done() + @UserPagesController.settingsPage @req, @res + + describe 'when ldap.updateUserDetailsOnLogin is true', -> + + beforeEach -> + @settings.ldap = {updateUserDetailsOnLogin: true} + + afterEach -> + delete @settings.ldap + + it 'should set "shouldAllowEditingDetails" to false', (done) -> + @res.render = (page, opts)=> + opts.shouldAllowEditingDetails.should.equal false + done() + @UserPagesController.settingsPage @req, @res + + describe 'when saml.updateUserDetailsOnLogin is true', -> + + beforeEach -> + @settings.saml = {updateUserDetailsOnLogin: true} + + afterEach -> + delete @settings.saml + + it 'should set "shouldAllowEditingDetails" to false', (done) -> + @res.render = (page, opts)=> + opts.shouldAllowEditingDetails.should.equal false + done() + @UserPagesController.settingsPage @req, @res + describe "activateAccountPage", -> beforeEach -> @req.query.user_id = @user_id From acce8853bac5023d02741c1dd3b88d78cdba2bb7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 Nov 2016 15:24:50 +0000 Subject: [PATCH 07/15] Remove redundant `baseUrl` from plans page. --- .../Features/Subscription/SubscriptionController.coffee | 2 -- services/web/app/views/subscriptions/plans.jade | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index d864cac1d1..84de417bb6 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -14,7 +14,6 @@ module.exports = SubscriptionController = plansPage: (req, res, next) -> plans = SubscriptionViewModelBuilder.buildViewModel() - baseUrl = "" viewName = "subscriptions/plans" if req.query.v? viewName = "#{viewName}_#{req.query.v}" @@ -26,7 +25,6 @@ module.exports = SubscriptionController = res.render viewName, title: "plans_and_pricing" plans: plans - baseUrl: baseUrl gaExperiments: Settings.gaExperiments.plansPage recomendedCurrency:recomendedCurrency shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27'))) diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade index 16376c7e72..88584dba76 100644 --- a/services/web/app/views/subscriptions/plans.jade +++ b/services/web/app/views/subscriptions/plans.jade @@ -100,7 +100,7 @@ block content li br a.btn.btn-info( - ng-href="#{baseUrl}/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')" + ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')" ) span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")} span(ng-show="ui.view == 'annual'") #{translate("buy_now")} @@ -124,7 +124,7 @@ block content li br a.btn.btn-info( - ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('professional')" + ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('professional')" ) span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")} span(ng-show="ui.view == 'annual'") #{translate("buy_now")} @@ -166,7 +166,7 @@ block content li br a.btn.btn-info( - ng-href="#{baseUrl}/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}¤cy={{currencyCode}}", + ng-href="/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}¤cy={{currencyCode}}", ng-click="signUpNowClicked('student')" ) #{translate("start_free_trial")} @@ -189,7 +189,7 @@ block content li br a.btn.btn-info( - ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}¤cy={{currencyCode}}", + ng-href="/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}¤cy={{currencyCode}}", ng-click="signUpNowClicked('student')" ) #{translate("buy_now")} From d15659ed8541edfb1e8c651be9d735b53e708375 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 28 Nov 2016 12:01:11 +0000 Subject: [PATCH 08/15] check user is present before accessing email --- services/web/app/views/sentry.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/sentry.jade b/services/web/app/views/sentry.jade index 4d838cd698..0a51686015 100644 --- a/services/web/app/views/sentry.jade +++ b/services/web/app/views/sentry.jade @@ -70,7 +70,7 @@ // whitelistUrls: ['example.com/scripts/'] }).install(); } - - if (typeof(user) != "undefined" && typeof (user.email) != "undefined") + - if (user && typeof(user) != "undefined" && typeof (user.email) != "undefined") script(type="text/javascript"). if (typeof(Raven) != "undefined" && Raven.setUserContext) { Raven.setUserContext({email: '#{user.email}'}); From 853dad95f376ac5cac594ba1c3251e7923ccfeb7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 28 Nov 2016 12:01:32 +0000 Subject: [PATCH 09/15] After account delete, bounce to login after 1-second delay --- services/web/public/coffee/main/account-settings.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee index 05fd4a6ca9..2f1b8208e7 100644 --- a/services/web/public/coffee/main/account-settings.coffee +++ b/services/web/public/coffee/main/account-settings.coffee @@ -64,7 +64,11 @@ define [ $scope.state.inflight = false $scope.state.error = false $scope.state.invalidCredentials = false - window.location = "/" + setTimeout( + () -> + window.location = "/login" + , 1000 + ) .error (data, status) -> $scope.state.inflight = false if status == 403 From a03ac8fe727770bda24c51c819d40b15a21f34d2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 28 Nov 2016 12:37:53 +0000 Subject: [PATCH 10/15] More robust session destruction after deleting user account. --- .../app/coffee/Features/User/UserController.coffee | 10 ++++++++-- .../coffee/User/UserControllerTests.coffee | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 27e37b91be..639e565d27 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -33,8 +33,14 @@ module.exports = UserController = if err? logger.err {user_id}, "error while deleting user account" return next(err) - req.session?.destroy() - res.sendStatus(200) + sessionId = req.sessionID + req.logout?() + req.session.destroy (err) -> + if err? + logger.err err: err, 'error destorying session' + return next(err) + UserSessionsManager.untrackSession(user, sessionId) + res.sendStatus(200) unsubscribe: (req, res)-> user_id = AuthenticationController.getLoggedInUserId(req) diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee index 96ba614d22..f2f886a4a6 100644 --- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee @@ -89,6 +89,8 @@ describe "UserController", -> beforeEach -> @req.body.password = 'wat' + @req.logout = sinon.stub() + @req.session.destroy = sinon.stub().callsArgWith(0, null) @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user._id) @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, null) @@ -159,6 +161,17 @@ describe "UserController", -> done() @UserController.tryDeleteUser @req, @res, @next + describe 'when session.destroy produces an error', -> + + beforeEach -> + @req.session.destroy = sinon.stub().callsArgWith(0, new Error('woops')) + + it 'should call next with an error', (done) -> + @next = (err) => + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + done() + @UserController.tryDeleteUser @req, @res, @next describe "unsubscribe", -> From 68ddf04fe81bf3c4429d7c555de53c7dfcd207e1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 28 Nov 2016 19:19:00 +0000 Subject: [PATCH 11/15] to string record event check --- .../web/app/coffee/Features/Analytics/AnalyticsManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee index 6840f8fa88..9f34d6d9d1 100644 --- a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee +++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee @@ -18,7 +18,7 @@ module.exports = recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) -> - if user_id == settings.smokeTest?.userId + if user_id+"" == settings.smokeTest?.userId+"" return callback() opts = body: From cf94429e059ad31ca56731224f95068feb99d404 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 29 Nov 2016 16:04:20 +0000 Subject: [PATCH 12/15] Add a `user_email` option to the nav "Account" dropdown. --- services/web/app/views/layout/navbar.jade | 3 +++ services/web/config/settings.defaults.coffee | 4 ++++ .../web/public/stylesheets/components/dropdowns.less | 9 ++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index e0a89fdea7..3cd6587592 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -35,6 +35,9 @@ nav.navbar.navbar-default each child in item.dropdown if child.divider li.divider + else if child.user_email + li + div.subdued #{getUserEmail()} else li if child.url diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 1e858253e4..e3b842a0d2 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -347,6 +347,10 @@ module.exports = settings = text: "Account" only_when_logged_in: true dropdown: [{ + user_email: true + },{ + divider: true + }, { text: "Account Settings" url: "/user/settings" }, { diff --git a/services/web/public/stylesheets/components/dropdowns.less b/services/web/public/stylesheets/components/dropdowns.less index 2b718026a1..7adbfe49d7 100755 --- a/services/web/public/stylesheets/components/dropdowns.less +++ b/services/web/public/stylesheets/components/dropdowns.less @@ -58,8 +58,8 @@ .nav-divider(@dropdown-divider-bg); } - // Links within the dropdown menu - > li > a { + // Links and other items within the dropdown menu + > li > a,div { display: block; padding: 3px 20px; clear: both; @@ -67,8 +67,11 @@ line-height: @line-height-base; color: @dropdown-link-color; white-space: nowrap; // prevent links from randomly breaking onto new lines + &.subdued { + color: #7a7a7a + } .subdued { - color: #7a7a7a + color: #7a7a7a } } } From d38890e9f43eaa3cc5fa530e12bdf1fa53a43457 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 30 Nov 2016 09:41:58 +0000 Subject: [PATCH 13/15] Add the `rolling` option to session --- services/web/app/coffee/infrastructure/Server.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 6ee9390b93..43683bdd4e 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -90,6 +90,7 @@ webRouter.use session secure: Settings.secureCookie store: sessionStore key: Settings.cookieName + rolling: true # passport webRouter.use passport.initialize() From ef115e2f91c004e1d26fa8768720179dcbf005a6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 30 Nov 2016 11:25:47 +0000 Subject: [PATCH 14/15] add a 2x version of the spellcheck-underline image. --- .../web/public/img/spellcheck-underline@2x.png | Bin 0 -> 1074 bytes services/web/public/stylesheets/app/editor.less | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 services/web/public/img/spellcheck-underline@2x.png diff --git a/services/web/public/img/spellcheck-underline@2x.png b/services/web/public/img/spellcheck-underline@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..957225b752ea0799932d3121274db2a5625044c5 GIT binary patch literal 1074 zcmZ{kv2W8r6vi(Cg%HRD10awrGq4b!oiv4<919wkCK4%82$HkaO)g+z`z+s8ait2W z-4F{iVgQMi3W_Gfo$WWGI3wzp-txi{Jk`PNZWl2$GqbNy| z9*)MSXWmP2-fPHD7z+@DlgUJxR233FhFZN|hiU~@DrN2{(`}#GQQ4>0r<2U)F)%#SF%noI11#`BrBdDjO+kviV7D&%o)#@_a7xjPQ-BJoPddu zDHa!Fx<;K>6m`hR8E>05!$w8bmdlz}uBv@aMVf|c%R0<><{8FxGsHF{VV@A!=sFR0 zJ<*k_Ne%SJqR7l%PWd#XgWNmW&KU!1tF$X_-lzjt!4c^Oo_WCrhnu+%%~(t zM$S@$`M7aw#-zn8U@ZdDVhRwS0mXH)Ovl51oXc$;F4VJJlei>&7}!HB$`vCiDeI1- z4CYam(os@+!s$@# Date: Wed, 30 Nov 2016 14:34:50 +0000 Subject: [PATCH 15/15] Update to modern images --- .../web/public/img/spellcheck-underline.png | Bin 111 -> 2872 bytes .../public/img/spellcheck-underline@2x.png | Bin 1074 -> 3003 bytes .../web/public/stylesheets/app/editor.less | 1 + 3 files changed, 1 insertion(+) diff --git a/services/web/public/img/spellcheck-underline.png b/services/web/public/img/spellcheck-underline.png index 07d7c5ce0e0a1078c644422400f343cb0e73f45e..0b3b38904afcc2e7b2599cb6e63196db2696ebbe 100644 GIT binary patch literal 2872 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 literal 1074 zcmZ{kv2W8r6vi(Cg%HRD10awrGq4b!oiv4<919wkCK4%82$HkaO)g+z`z+s8ait2W z-4F{iVgQMi3W_Gfo$WWGI3wzp-txi{Jk`PNZWl2$GqbNy| z9*)MSXWmP2-fPHD7z+@DlgUJxR233FhFZN|hiU~@DrN2{(`}#GQQ4>0r<2U)F)%#SF%noI11#`BrBdDjO+kviV7D&%o)#@_a7xjPQ-BJoPddu zDHa!Fx<;K>6m`hR8E>05!$w8bmdlz}uBv@aMVf|c%R0<><{8FxGsHF{VV@A!=sFR0 zJ<*k_Ne%SJqR7l%PWd#XgWNmW&KU!1tF$X_-lzjt!4c^Oo_WCrhnu+%%~(t zM$S@$`M7aw#-zn8U@ZdDVhRwS0mXH)Ovl51oXc$;F4VJJlei>&7}!HB$`vCiDeI1- z4CYam(os@+!s$@#