From d0f77abc70650c411e6430f9269c06ab6cfed6a5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jul 2018 13:26:52 +0100 Subject: [PATCH] Update email in v1 when setting default in v2 --- .../app/coffee/Features/Errors/Errors.coffee | 8 ++ .../Features/User/UserEmailsController.coffee | 11 +- .../coffee/Features/User/UserUpdater.coffee | 41 ++++++ .../acceptance/coffee/UserEmailsTests.coffee | 128 ++++++++++++++++++ .../coffee/helpers/MockV1Api.coffee | 6 + .../User/UserEmailsControllerTests.coffee | 5 +- 6 files changed, 194 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index 25b3f3e06d..adf6fcaf12 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -68,6 +68,13 @@ V1ConnectionError = (message) -> return error V1ConnectionError.prototype.__proto___ = Error.prototype +UnconfirmedEmailError = (message) -> + error = new Error(message) + error.name = "UnconfirmedEmailError" + error.__proto__ = UnconfirmedEmailError.prototype + return error +UnconfirmedEmailError.prototype.__proto___ = Error.prototype + module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError @@ -79,3 +86,4 @@ module.exports = Errors = V1HistoryNotSyncedError: V1HistoryNotSyncedError ProjectHistoryDisabledError: ProjectHistoryDisabledError V1ConnectionError: V1ConnectionError + UnconfirmedEmailError: UnconfirmedEmailError diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index 1afb60b99b..32fabe4c6d 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -47,9 +47,14 @@ module.exports = UserEmailsController = email = EmailHelper.parseEmail(req.body.email) return res.sendStatus 422 unless email? - UserUpdater.setDefaultEmailAddress userId, email, (error)-> - return next(error) if error? - res.sendStatus 200 + UserUpdater.updateV1AndSetDefaultEmailAddress userId, email, (error)-> + if error? + if error instanceof Errors.UnconfirmedEmailError + return res.sendStatus 409 + else + return next(error) + else + return res.sendStatus 200 endorse: (req, res, next) -> diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 948be16d41..22ec235f91 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -8,6 +8,8 @@ UserGetter = require("./UserGetter") { addAffiliation, removeAffiliation } = require("./UserAffiliationsManager") EmailHelper = require "../Helpers/EmailHelper" Errors = require "../Errors/Errors" +Settings = require "settings-sharelatex" +request = require 'request' module.exports = UserUpdater = updateUser: (query, update, callback = (error) ->) -> @@ -106,6 +108,45 @@ module.exports = UserUpdater = return callback(new Error('Default email does not belong to user')) callback() + updateV1AndSetDefaultEmailAddress: (userId, email, callback) -> + @updateEmailAddressInV1 userId, email, (error) => + return callback(error) if error? + @setDefaultEmailAddress userId, email, callback + + updateEmailAddressInV1: (userId, newEmail, callback) -> + if !Settings.apis?.v1?.url? + return callback() + UserGetter.getUser userId, { 'overleaf.id': 1, emails: 1 }, (error, user) -> + return callback(error) if error? + return callback(new Errors.NotFoundError('no user found')) if !user? + if !user.overleaf?.id? + return callback() + newEmailIsConfirmed = false + for email in user.emails + if email.email == newEmail and email.confirmedAt? + newEmailIsConfirmed = true + break + if !newEmailIsConfirmed + return callback(new Errors.UnconfirmedEmailError("can't update v1 with unconfirmed email")) + request { + baseUrl: Settings.apis.v1.url + url: "/api/v1/sharelatex/users/#{user.overleaf.id}/email" + method: 'PUT' + auth: + user: Settings.apis.v1.user + pass: Settings.apis.v1.pass + sendImmediately: true + json: { email: newEmail }, + timeout: 5 * 1000 + }, (error, response, body) -> + if error? + error = new Errors.V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED' + return callback(error) + if 200 <= response.statusCode < 300 + return callback() + else + return callback new Error("non-success code from v1: #{response.statusCode}") + confirmEmail: (userId, email, callback) -> email = EmailHelper.parseEmail(email) return callback(new Error('invalid email')) if !email? diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 71837f4633..2ae078ff6c 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -302,3 +302,131 @@ describe "UserEmails", -> expect(tokens.length).to.equal 0 cb() ], done + + describe 'setting a default email', -> + it 'should update confirmed emails', (done) -> + token = null + async.series [ + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'new-confirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + # Mark the email as confirmed + db.users.update { + 'emails.email': 'new-confirmed-default@example.com' + }, { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'new-confirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + (cb) => + @user.request { url: '/user/emails', json: true }, (error, response, body) -> + expect(response.statusCode).to.equal 200 + expect(body[0].confirmedAt).to.not.exist + expect(body[0].default).to.equal false + expect(body[1].confirmedAt).to.exist + expect(body[1].default).to.equal true + cb() + ], done + + it 'should not allow changing unconfirmed emails in v1', (done) -> + token = null + async.series [ + (cb) => + db.users.update { + _id: ObjectId(@user._id) + }, { + $set: { + 'overleaf.id': 42 + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'new-unconfirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'new-unconfirmed-default@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 409 + cb() + (cb) => + @user.request { url: '/user/emails', json: true }, (error, response, body) -> + expect(body[0].default).to.equal true + expect(body[1].default).to.equal false + cb() + ], done + + it 'should update the email in v2', (done) -> + token = null + async.series [ + (cb) => + db.users.update { + _id: ObjectId(@user._id) + }, { + $set: { + 'overleaf.id': 42 + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'new-confirmed-default-in-v1@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + # Mark the email as confirmed + db.users.update { + 'emails.email': 'new-confirmed-default-in-v1@example.com' + }, { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/default', + json: + email: 'new-confirmed-default-in-v1@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + ], (error) => + return done(error) if error? + expect( + MockV1Api.updateEmail.calledWith(42, 'new-confirmed-default-in-v1@example.com') + ).to.equal true + done() diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index 7599230b09..a54d556935 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -28,6 +28,8 @@ module.exports = MockV1Api = affiliations: [] + updateEmail: sinon.stub() + setAffiliations: (affiliations) -> @affiliations = affiliations run: () -> @@ -64,6 +66,10 @@ module.exports = MockV1Api = app.get '/university/domains', (req, res, next) -> res.json [] + app.put '/api/v1/sharelatex/users/:id/email', (req, res, next) => + @updateEmail parseInt(req.params.id), req.body.email + res.sendStatus 200 + app.listen 5000, (error) -> throw error if error? .on "error", (error) -> diff --git a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee index 384623a2b5..2f67131c65 100644 --- a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee @@ -24,6 +24,7 @@ describe "UserEmailsController", -> addEmailAddress: sinon.stub() removeEmailAddress: sinon.stub() setDefaultEmailAddress: sinon.stub() + updateV1AndSetDefaultEmailAddress: sinon.stub() @EmailHelper = parseEmail: sinon.stub() @endorseAffiliation = sinon.stub().yields() @@ -126,13 +127,13 @@ describe "UserEmailsController", -> @EmailHelper.parseEmail.returns @email it 'sets default email', (done) -> - @UserUpdater.setDefaultEmailAddress.callsArgWith 2, null + @UserUpdater.updateV1AndSetDefaultEmailAddress.callsArgWith 2, null @UserEmailsController.setDefault @req, sendStatus: (code) => code.should.equal 200 assertCalledWith @EmailHelper.parseEmail, @email - assertCalledWith @UserUpdater.setDefaultEmailAddress, @user._id, @email + assertCalledWith @UserUpdater.updateV1AndSetDefaultEmailAddress, @user._id, @email done() it 'handles email parse error', (done) ->