Merge pull request #602 from sharelatex/ta-add-multi-emails

Add Multiple Emails
This commit is contained in:
Timothée Alby 2018-06-05 17:07:12 +02:00 committed by GitHub
commit e64a704341
8 changed files with 312 additions and 33 deletions

View file

@ -21,6 +21,10 @@ module.exports = UserGetter =
db.users.findOne query, projection, callback db.users.findOne query, projection, callback
getUserEmail: (userId, callback = (error, email) ->) ->
@getUser userId, { email: 1 }, (error, user) ->
callback(error, user?.email)
getUserByMainEmail: (email, projection, callback = (error, user) ->) -> getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
email = email.trim() email = email.trim()
if arguments.length == 2 if arguments.length == 2
@ -28,6 +32,18 @@ module.exports = UserGetter =
projection = {} projection = {}
db.users.findOne email: email, projection, callback db.users.findOne email: email, projection, callback
getUserByAnyEmail: (email, projection, callback = (error, user) ->) ->
email = email.trim()
if arguments.length == 2
callback = projection
projection = {}
db.users.findOne 'emails.email': email, projection, (error, user) =>
return callback(error, user) if error? or user?
# While multiple emails are being rolled out, check for the main email as
# well
@getUserByMainEmail email, projection, callback
getUsers: (user_ids, projection, callback = (error, users) ->) -> getUsers: (user_ids, projection, callback = (error, users) ->) ->
try try
user_ids = user_ids.map (u) -> ObjectId(u.toString()) user_ids = user_ids.map (u) -> ObjectId(u.toString())
@ -48,7 +64,9 @@ module.exports = UserGetter =
[ [
'getUser', 'getUser',
'getUserEmail',
'getUserByMainEmail', 'getUserByMainEmail',
'getUserByAnyEmail',
'getUsers', 'getUsers',
'getUserOrUserStubById' 'getUserOrUserStubById'
].map (method) -> ].map (method) ->

View file

@ -2,6 +2,7 @@ logger = require("logger-sharelatex")
mongojs = require("../../infrastructure/mongojs") mongojs = require("../../infrastructure/mongojs")
metrics = require("metrics-sharelatex") metrics = require("metrics-sharelatex")
db = mongojs.db db = mongojs.db
async = require("async")
ObjectId = mongojs.ObjectId ObjectId = mongojs.ObjectId
UserGetter = require("./UserGetter") UserGetter = require("./UserGetter")
@ -11,23 +12,89 @@ module.exports = UserUpdater =
query = _id: ObjectId(query) query = _id: ObjectId(query)
else if query instanceof ObjectId else if query instanceof ObjectId
query = _id: query query = _id: query
else if typeof query._id == "string"
query._id = ObjectId(query._id)
db.users.update query, update, callback db.users.update query, update, callback
changeEmailAddress: (user_id, newEmail, callback)-> #
self = @ # DEPRECATED
logger.log user_id:user_id, newEmail:newEmail, "updaing email address of user" #
UserGetter.getUserByMainEmail newEmail, (error, user) -> # Change the user's main email address by adding a new email, switching the
if user? # default email and removing the old email. Prefer manipulating multiple
return callback({message:"alread_exists"}) # emails and the default rather than calling this method directly
self.updateUser user_id.toString(), { #
$set: { "email": newEmail}, changeEmailAddress: (userId, newEmail, callback)->
}, (err) -> logger.log userId: userId, newEmail: newEmail, "updaing email address of user"
if err?
logger.err err:err, "problem updating users email" oldEmail = null
return callback(err) async.series [
(cb) ->
UserGetter.getUserEmail userId, (error, email) ->
oldEmail = email
cb(error)
(cb) -> UserUpdater.addEmailAddress userId, newEmail, cb
(cb) -> UserUpdater.setDefaultEmailAddress userId, newEmail, cb
(cb) -> UserUpdater.removeEmailAddress userId, oldEmail, cb
], callback
# Add a new email address for the user. Email cannot be already used by this
# or any other user
addEmailAddress: (userId, newEmail, callback) ->
@_ensureUniqueEmailAddress newEmail, (error) =>
return callback(error) if error?
update = $push: emails: email: newEmail, createdAt: new Date()
@updateUser userId, update, (error) ->
if error?
logger.err error: error, 'problem updating users emails'
return callback(error)
callback() callback()
metrics.timeAsyncMethod UserUpdater, 'updateUser', 'mongo.UserUpdater', logger # remove one of the user's email addresses. The email cannot be the user's
# default email address
removeEmailAddress: (userId, email, callback) ->
query = _id: userId, email: $ne: email
update = $pull: emails: email: email
@updateUser query, update, (error, res) ->
if error?
logger.err error:error, 'problem removing users email'
return callback(error)
if res.nMatched == 0
return callback(new Error('Cannot remove default email'))
callback()
# set the default email address by setting the `email` attribute. The email
# must be one of the user's multiple emails (`emails` attribute)
setDefaultEmailAddress: (userId, email, callback) ->
query = _id: userId, 'emails.email': email
update = $set: email: email
@updateUser query, update, (error, res) ->
if error?
logger.err error:error, 'problem setting default emails'
return callback(error)
if res.nMatched == 0
return callback(new Error('Default email does not belong to user'))
callback()
# check for duplicate email address. This is also enforced at the DB level
_ensureUniqueEmailAddress: (newEmail, callback) ->
UserGetter.getUserByAnyEmail newEmail, (error, user) ->
return callback(message: 'alread_exists') if user?
callback()
[
'updateUser'
'changeEmailAddress'
'setDefaultEmailAddress'
'addEmailAddress'
'removeEmailAddress'
'_ensureUniqueEmailAddress'
].map (method) ->
metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger)

View file

@ -8,6 +8,10 @@ ObjectId = Schema.ObjectId
UserSchema = new Schema UserSchema = new Schema
email : {type : String, default : ''} email : {type : String, default : ''}
emails: [{
email: { type : String, default : '' },
createdAt: { type : Date, default: () -> new Date() }
}],
first_name : {type : String, default : ''} first_name : {type : String, default : ''}
last_name : {type : String, default : ''} last_name : {type : String, default : ''}
role : {type : String, default : ''} role : {type : String, default : ''}

View file

@ -146,6 +146,15 @@ describe "LoginViaRegistration", ->
describe "[Security] Trying to register/login as another user", -> describe "[Security] Trying to register/login as another user", ->
it 'should not allow sign in with secondary email', (done) ->
secondaryEmail = "acceptance-test-secondary@example.com"
@user1.addEmail secondaryEmail, (err) =>
@user1.loginWith secondaryEmail, (err) =>
expect(err?).to.equal false
@user1.isLoggedIn (err, isLoggedIn) ->
expect(isLoggedIn).to.equal false
done()
it 'should have user1 login', (done) -> it 'should have user1 login', (done) ->
@user1.login (err) -> @user1.login (err) ->
expect(err?).to.equal false expect(err?).to.equal false

View file

@ -0,0 +1,28 @@
should = require('chai').should()
async = require("async")
User = require "./helpers/User"
describe 'SettingsPage', ->
before (done) ->
@user = new User()
async.series [
@user.ensureUserExists.bind(@user)
@user.login.bind(@user)
@user.activateSudoMode.bind(@user)
], done
it 'load settigns page', (done) ->
@user.getUserSettingsPage (err, statusCode) ->
statusCode.should.equal 200
done()
it 'update main email address', (done) ->
newEmail = 'foo@bar.com'
@user.updateSettings email: newEmail, (error) =>
should.not.exist error
@user.get (error, user) ->
user.email.should.equal newEmail
user.emails.length.should.equal 1
user.emails[0].email.should.equal newEmail
done()

View file

@ -3,13 +3,18 @@ _ = require("underscore")
settings = require("settings-sharelatex") settings = require("settings-sharelatex")
{db, ObjectId} = require("../../../../app/js/infrastructure/mongojs") {db, ObjectId} = require("../../../../app/js/infrastructure/mongojs")
UserModel = require("../../../../app/js/models/User").User UserModel = require("../../../../app/js/models/User").User
UserUpdater = require("../../../../app/js/Features/User/UserUpdater")
AuthenticationManager = require("../../../../app/js/Features/Authentication/AuthenticationManager") AuthenticationManager = require("../../../../app/js/Features/Authentication/AuthenticationManager")
count = 0 count = 0
class User class User
constructor: (options = {}) -> constructor: (options = {}) ->
@email = "acceptance-test-#{count}@example.com" @emails = [
email: "acceptance-test-#{count}@example.com"
createdAt: new Date()
]
@email = @emails[0].email
@password = "acceptance-test-#{count}-password" @password = "acceptance-test-#{count}-password"
count++ count++
@jar = request.jar() @jar = request.jar()
@ -17,14 +22,20 @@ class User
jar: @jar jar: @jar
}) })
get: (callback = (error, user)->) ->
db.users.findOne { _id: ObjectId(@_id) }, callback
login: (callback = (error) ->) -> login: (callback = (error) ->) ->
@loginWith(@email, callback)
loginWith: (email, callback = (error) ->) ->
@ensureUserExists (error) => @ensureUserExists (error) =>
return callback(error) if error? return callback(error) if error?
@getCsrfToken (error) => @getCsrfToken (error) =>
return callback(error) if error? return callback(error) if error?
@request.post { @request.post {
url: "/login" url: "/login"
json: { @email, @password } json: { email, @password }
}, callback }, callback
ensureUserExists: (callback = (error) ->) -> ensureUserExists: (callback = (error) ->) ->
@ -33,11 +44,14 @@ class User
UserModel.findOneAndUpdate filter, {}, options, (error, user) => UserModel.findOneAndUpdate filter, {}, options, (error, user) =>
return callback(error) if error? return callback(error) if error?
AuthenticationManager.setUserPassword user._id, @password, (error) => AuthenticationManager.setUserPassword user._id, @password, (error) =>
return callback(error) if error?
UserUpdater.updateUser user._id, $set: emails: @emails, (error) =>
return callback(error) if error? return callback(error) if error?
@id = user?._id?.toString() @id = user?._id?.toString()
@_id = user?._id?.toString() @_id = user?._id?.toString()
@first_name = user?.first_name @first_name = user?.first_name
@referal_id = user?.referal_id @referal_id = user?.referal_id
callback(null, @password) callback(null, @password)
setFeatures: (features, callback = (error) ->) -> setFeatures: (features, callback = (error) ->) ->
@ -62,6 +76,10 @@ class User
@_id = user?._id?.toString() @_id = user?._id?.toString()
callback() callback()
addEmail: (email, callback = (error) ->) ->
@emails.push(email: email, createdAt: new Date())
UserUpdater.addEmailAddress @id, email, callback
ensure_admin: (callback = (error) ->) -> ensure_admin: (callback = (error) ->) ->
db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback
@ -226,6 +244,23 @@ class User
return callback(error) if error? return callback(error) if error?
callback(null, response.statusCode) callback(null, response.statusCode)
activateSudoMode: (callback = (error)->) ->
@getCsrfToken (error) =>
return callback(error) if error?
@request.post {
uri: '/confirm-password',
json:
password: @password
}, callback
updateSettings: (newSettings, callback = (error, response, body) ->) ->
@getCsrfToken (error) =>
return callback(error) if error?
@request.post {
url: '/user/settings'
json: newSettings
}, callback
getProjectListPage: (callback=(error, statusCode)->) -> getProjectListPage: (callback=(error, statusCode)->) ->
@getCsrfToken (error) => @getCsrfToken (error) =>
return callback(error) if error? return callback(error) if error?

View file

@ -56,3 +56,21 @@ describe "UserGetter", ->
@findOne.called.should.equal true @findOne.called.should.equal true
@findOne.calledWith(email: email).should.equal true @findOne.calledWith(email: email).should.equal true
done() done()
describe "getUserByAnyEmail", ->
it "query user for any email", (done)->
email = 'hello@world.com'
projection = emails: 1
@UserGetter.getUserByAnyEmail " #{email} ", projection, (error, user) =>
@findOne.calledWith('emails.email': email, projection).should.equal true
user.should.deep.equal @fakeUser
done()
it "checks main email as well", (done)->
@findOne.callsArgWith(2, null, null)
email = 'hello@world.com'
projection = emails: 1
@UserGetter.getUserByAnyEmail " #{email} ", projection, (error, user) =>
@findOne.calledTwice.should.equal true
@findOne.calledWith(email: email, projection).should.equal true
done()

View file

@ -15,35 +15,135 @@ describe "UserUpdater", ->
db:{} db:{}
ObjectId:(id)-> return id ObjectId:(id)-> return id
@UserGetter = @UserGetter =
getUserByMainEmail: sinon.stub() getUserEmail: sinon.stub()
getUserByAnyEmail: sinon.stub()
@logger = err: sinon.stub(), log: ->
@UserUpdater = SandboxedModule.require modulePath, requires: @UserUpdater = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings "settings-sharelatex":@settings
"logger-sharelatex": log:-> "logger-sharelatex": @logger
"./UserGetter": @UserGetter "./UserGetter": @UserGetter
"../../infrastructure/mongojs":@mongojs "../../infrastructure/mongojs":@mongojs
"metrics-sharelatex": timeAsyncMethod: sinon.stub() "metrics-sharelatex": timeAsyncMethod: sinon.stub()
@stubbedUser = @stubbedUser =
_id: "3131231"
name:"bob" name:"bob"
email:"hello@world.com" email:"hello@world.com"
@user_id = "3131231"
@newEmail = "bob@bob.com" @newEmail = "bob@bob.com"
describe "changeEmailAddress", -> describe 'changeEmailAddress', ->
beforeEach -> beforeEach ->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2) @UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email)
@UserUpdater.addEmailAddress = sinon.stub().callsArgWith(2)
@UserUpdater.setDefaultEmailAddress = sinon.stub().callsArgWith(2)
@UserUpdater.removeEmailAddress = sinon.stub().callsArgWith(2)
it "should check if the new email already has an account", (done)-> it 'change email', (done)->
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @stubbedUser) @UserUpdater.changeEmailAddress @stubbedUser._id, @newEmail, (err)=>
@UserUpdater.changeEmailAddress @user_id, @stubbedUser.email, (err)=> should.not.exist(err)
@UserUpdater.updateUser.called.should.equal false @UserUpdater.addEmailAddress.calledWith(
@stubbedUser._id, @newEmail
).should.equal true
@UserUpdater.setDefaultEmailAddress.calledWith(
@stubbedUser._id, @newEmail
).should.equal true
@UserUpdater.removeEmailAddress.calledWith(
@stubbedUser._id, @stubbedUser.email
).should.equal true
done()
it 'handle error', (done)->
@UserUpdater.removeEmailAddress.callsArgWith(2, new Error('nope'))
@UserUpdater.changeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
describe 'addEmailAddress', ->
beforeEach ->
@UserUpdater._ensureUniqueEmailAddress = sinon.stub().callsArgWith(1)
it 'add email', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null)
@UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=>
@UserUpdater._ensureUniqueEmailAddress.called.should.equal true
should.not.exist(err)
@UserUpdater.updateUser.calledWith(
@stubbedUser._id,
$push: { emails: { email: @newEmail, createdAt: sinon.match.date } }
).should.equal true
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=>
@logger.err.called.should.equal true
should.exist(err)
done()
describe 'removeEmailAddress', ->
it 'remove email', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1)
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@UserUpdater.updateUser.calledWith(
{ _id: @stubbedUser._id, email: { $ne: @newEmail } },
$pull: { emails: { email: @newEmail } }
).should.equal true
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
it 'handle missed update', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 0)
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
describe 'setDefaultEmailAddress', ->
it 'set default', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1)
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@UserUpdater.updateUser.calledWith(
{ _id: @stubbedUser._id, 'emails.email': @newEmail },
$set: { email: @newEmail }
).should.equal true
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
it 'handle missed update', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 0)
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err) should.exist(err)
done() done()
it "should set the users password", (done)-> describe '_ensureUniqueEmailAddress', ->
@UserGetter.getUserByMainEmail.callsArgWith(1, null) it 'should return error if existing user is found', (done)->
@UserUpdater.changeEmailAddress @user_id, @newEmail, (err)=> @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @stubbedUser)
@UserUpdater.updateUser.calledWith(@user_id, $set: { "email": @newEmail}).should.equal true @UserUpdater._ensureUniqueEmailAddress @newEmail, (err)=>
should.exist(err)
done() done()
it 'should return null if no user is found', (done)->
@UserGetter.getUserByAnyEmail.callsArgWith(1)
@UserUpdater._ensureUniqueEmailAddress @newEmail, (err)=>
should.not.exist(err)
done()