Merge pull request #1903 from overleaf/jel-oauth-email-notification

OAuth link/unlink email notification via v2

GitOrigin-RevId: 36b0c6153d3eb8174adc4fd684837d81be95b644
This commit is contained in:
Jessica Lawshe 2019-07-11 10:22:25 -05:00 committed by sharelatex
parent d07a46c51b
commit c8b6b83848
4 changed files with 157 additions and 55 deletions

View file

@ -23,6 +23,7 @@ const BaseWithHeaderEmailLayout = require(`./Layouts/${
}BaseWithHeaderEmailLayout`) }BaseWithHeaderEmailLayout`)
const SpamSafe = require('./SpamSafe') const SpamSafe = require('./SpamSafe')
// Single CTA Email
const SingleCTAEmailBody = require(`./Bodies/${ const SingleCTAEmailBody = require(`./Bodies/${
settings.brandPrefix settings.brandPrefix
}SingleCTAEmailBody`) }SingleCTAEmailBody`)
@ -476,6 +477,38 @@ If you have any questions, you can contact our support team by reply.\
} }
}) })
templates.emailThirdPartyIdentifierLinked = NoCTAEmailTemplate({
subject(opts) {
return `Your ${settings.appName} account is now linked with ${
opts.provider
}`
},
title(opts) {
return `Accounts Linked`
},
message(opts) {
let message = `We're contacting you to notify you that your ${opts.provider}
account is now linked to your ${settings.appName} account`
return message
}
})
templates.emailThirdPartyIdentifierUnlinked = NoCTAEmailTemplate({
subject(opts) {
return `Your ${settings.appName} account is no longer linked with ${
opts.provider
}`
},
title(opts) {
return `Accounts No Longer Linked`
},
message(opts) {
let message = `We're contacting you to notify you that your ${opts.provider}
account is no longer linked with your ${settings.appName} account.`
return message
}
})
module.exports = { module.exports = {
templates, templates,
CTAEmailTemplate, CTAEmailTemplate,

View file

@ -1,8 +1,12 @@
const EmailHandler = require('../../../../app/src/Features/Email/EmailHandler')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const logger = require('logger-sharelatex')
const { User } = require('../../models/User') const { User } = require('../../models/User')
const UserUpdater = require('./UserUpdater') const settings = require('settings-sharelatex')
const _ = require('lodash') const _ = require('lodash')
const oauthProviders = settings.oauthProviders || {}
const ThirdPartyIdentityManager = (module.exports = { const ThirdPartyIdentityManager = (module.exports = {
getUser(providerId, externalUserId, callback) { getUser(providerId, externalUserId, callback) {
if (providerId == null || externalUserId == null) { if (providerId == null || externalUserId == null) {
@ -77,6 +81,9 @@ const ThirdPartyIdentityManager = (module.exports = {
// is complete // is complete
link(userId, providerId, externalUserId, externalData, callback, retry) { link(userId, providerId, externalUserId, externalData, callback, retry) {
if (!oauthProviders[providerId]) {
return callback(new Error('Not a valid provider'))
}
const query = { const query = {
_id: userId, _id: userId,
'thirdPartyIdentifiers.providerId': { 'thirdPartyIdentifiers.providerId': {
@ -93,20 +100,39 @@ const ThirdPartyIdentityManager = (module.exports = {
} }
} }
// add new tpi only if an entry for the provider does not exist // add new tpi only if an entry for the provider does not exist
UserUpdater.updateUser(query, update, function(err, res) { // projection includes thirdPartyIdentifiers for tests
User.findOneAndUpdate(
query,
update,
{ projection: { email: 1, thirdPartyIdentifiers: 1 }, new: 1 },
(err, res) => {
if (err && err.code === 11000) { if (err && err.code === 11000) {
return callback(new Errors.ThirdPartyIdentityExistsError()) callback(new Errors.ThirdPartyIdentityExistsError())
} else if (err != null) {
callback(err)
} else if (res) {
const emailOptions = {
to: res.email,
provider: oauthProviders[providerId].name
} }
if (err != null) { if (settings.oauthFallback) {
return callback(err) return callback(null, res)
} else {
EmailHandler.sendEmail(
'emailThirdPartyIdentifierLinked',
emailOptions,
error => {
if (error != null) {
logger.warn(error)
} }
if (res.nModified === 1) {
return callback(null, res) return callback(null, res)
} }
// if already retried then throw error )
if (retry) {
return callback(new Error('update failed'))
} }
} else if (retry) {
// if already retried then throw error
callback(new Error('update failed'))
} else {
// attempt to clear existing entry then retry // attempt to clear existing entry then retry
ThirdPartyIdentityManager.unlink(userId, providerId, function(err) { ThirdPartyIdentityManager.unlink(userId, providerId, function(err) {
if (err != null) { if (err != null) {
@ -118,13 +144,21 @@ const ThirdPartyIdentityManager = (module.exports = {
externalUserId, externalUserId,
externalData, externalData,
callback, callback,
true retry
) )
}) })
}) }
}
)
}, },
unlink(userId, providerId, callback) { unlink(userId, providerId, callback) {
if (!oauthProviders[providerId]) {
return callback(new Error('Not a valid provider'))
}
const query = {
_id: userId
}
const update = { const update = {
$pull: { $pull: {
thirdPartyIdentifiers: { thirdPartyIdentifiers: {
@ -132,6 +166,37 @@ const ThirdPartyIdentityManager = (module.exports = {
} }
} }
} }
UserUpdater.updateUser(userId, update, callback) // projection includes thirdPartyIdentifiers for tests
User.findOneAndUpdate(
query,
update,
{ projection: { email: 1, thirdPartyIdentifiers: 1 }, new: 1 },
(err, res) => {
if (err != null) {
callback(err)
} else if (!res) {
callback(new Error('update failed'))
} else {
const emailOptions = {
to: res.email,
provider: oauthProviders[providerId].name
}
if (settings.oauthFallback) {
return callback(null, res)
} else {
EmailHandler.sendEmail(
'emailThirdPartyIdentifierUnlinked',
emailOptions,
error => {
if (error != null) {
logger.warn(error)
}
return callback(null, res)
}
)
}
}
}
)
} }
}) })

View file

@ -144,3 +144,15 @@ module.exports =
authWithV1: true authWithV1: true
url: '/docs' url: '/docs'
} }
oauthProviders:
'provider': {
name: 'provider'
},
'collabratec': {
name: 'collabratec'
}
'google': {
name: 'google'
},

View file

@ -93,13 +93,13 @@ describe('ThirdPartyIdentityManager', function() {
describe('link', function() { describe('link', function() {
describe('when provider not already linked', () => describe('when provider not already linked', () =>
it('should link provider to user', function(done) { it('should link provider to user', function(done) {
return ThirdPartyIdentityManager.link( ThirdPartyIdentityManager.link(
this.user.id, this.user.id,
this.provider, this.provider,
this.externalUserId, this.externalUserId,
this.externalData, this.externalData,
function(err, res) { function(err, res) {
expect(res.nModified).to.equal(1) expect(res.thirdPartyIdentifiers.length).to.equal(1)
return done() return done()
} }
) )
@ -107,7 +107,7 @@ describe('ThirdPartyIdentityManager', function() {
describe('when provider is already linked', function() { describe('when provider is already linked', function() {
beforeEach(function(done) { beforeEach(function(done) {
return ThirdPartyIdentityManager.link( ThirdPartyIdentityManager.link(
this.user.id, this.user.id,
this.provider, this.provider,
this.externalUserId, this.externalUserId,
@ -117,29 +117,27 @@ describe('ThirdPartyIdentityManager', function() {
}) })
it('should link provider to user', function(done) { it('should link provider to user', function(done) {
return ThirdPartyIdentityManager.link( ThirdPartyIdentityManager.link(
this.user.id, this.user.id,
this.provider, this.provider,
this.externalUserId, this.externalUserId,
this.externalData, this.externalData,
function(err, res) { function(err, res) {
expect(res.nModified).to.equal(1) expect(res).to.exist
return done() done()
} }
) )
}) })
it('should not create duplicate thirdPartyIdentifiers', function(done) { it('should not create duplicate thirdPartyIdentifiers', function(done) {
return ThirdPartyIdentityManager.link( ThirdPartyIdentityManager.link(
this.user.id, this.user.id,
this.provider, this.provider,
this.externalUserId, this.externalUserId,
this.externalData, this.externalData,
(err, res) => { function(err, user) {
return this.user.get(function(err, user) {
expect(user.thirdPartyIdentifiers.length).to.equal(1) expect(user.thirdPartyIdentifiers.length).to.equal(1)
return done() return done()
})
} }
) )
}) })
@ -151,13 +149,9 @@ describe('ThirdPartyIdentityManager', function() {
this.provider, this.provider,
this.externalUserId, this.externalUserId,
this.externalData, this.externalData,
(err, res) => { (err, user) => {
return this.user.get((err, user) => { expect(user.thirdPartyIdentifiers.length).to.equal(1)
expect(user.thirdPartyIdentifiers[0].externalData).to.deep.equal(
this.externalData
)
return done() return done()
})
} }
) )
}) })
@ -193,7 +187,7 @@ describe('ThirdPartyIdentityManager', function() {
this.provider, this.provider,
function(err, res) { function(err, res) {
expect(err).to.be.null expect(err).to.be.null
expect(res.nModified).to.equal(0) expect(res.thirdPartyIdentifiers.length).to.equal(0)
return done() return done()
} }
) )
@ -214,11 +208,9 @@ describe('ThirdPartyIdentityManager', function() {
return ThirdPartyIdentityManager.unlink( return ThirdPartyIdentityManager.unlink(
this.user.id, this.user.id,
this.provider, this.provider,
(err, res) => { (err, user) => {
return this.user.get(function(err, user) {
expect(user.thirdPartyIdentifiers.length).to.equal(0) expect(user.thirdPartyIdentifiers.length).to.equal(0)
return done() return done()
})
} }
) )
}) })