2019-05-29 05:21:06 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
2020-08-11 05:35:08 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
2020-10-06 06:31:34 -04:00
|
|
|
const { db } = require('../../infrastructure/mongodb')
|
|
|
|
const { normalizeQuery } = require('../Helpers/Mongo')
|
2020-10-30 04:10:50 -04:00
|
|
|
const metrics = require('@overleaf/metrics')
|
2019-05-29 05:21:06 -04:00
|
|
|
const async = require('async')
|
2020-08-12 10:18:45 -04:00
|
|
|
const { callbackify, promisify } = require('util')
|
2019-05-29 05:21:06 -04:00
|
|
|
const UserGetter = require('./UserGetter')
|
|
|
|
const {
|
|
|
|
addAffiliation,
|
2020-09-14 09:54:19 -04:00
|
|
|
removeAffiliation,
|
|
|
|
promises: InstitutionsAPIPromises
|
2019-05-29 05:21:06 -04:00
|
|
|
} = require('../Institutions/InstitutionsAPI')
|
2020-02-20 11:08:40 -05:00
|
|
|
const Features = require('../../infrastructure/Features')
|
2019-05-29 05:21:06 -04:00
|
|
|
const FeaturesUpdater = require('../Subscription/FeaturesUpdater')
|
2020-08-12 10:19:55 -04:00
|
|
|
const EmailHandler = require('../Email/EmailHandler')
|
2019-05-29 05:21:06 -04:00
|
|
|
const EmailHelper = require('../Helpers/EmailHelper')
|
|
|
|
const Errors = require('../Errors/Errors')
|
|
|
|
const NewsletterManager = require('../Newsletter/NewsletterManager')
|
2020-02-27 07:46:07 -05:00
|
|
|
const RecurlyWrapper = require('../Subscription/RecurlyWrapper')
|
2020-08-12 10:19:33 -04:00
|
|
|
const UserAuditLogHandler = require('./UserAuditLogHandler')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-09-29 10:05:12 -04:00
|
|
|
async function _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email) {
|
|
|
|
// send email to both old and new primary email
|
|
|
|
const emailOptions = {
|
|
|
|
actionDescribed: `the primary email address on your account was changed to ${email}`,
|
|
|
|
action: 'change of primary email address'
|
|
|
|
}
|
|
|
|
const toOld = Object.assign({}, emailOptions, { to: oldEmail })
|
|
|
|
const toNew = Object.assign({}, emailOptions, { to: email })
|
|
|
|
|
|
|
|
try {
|
|
|
|
await EmailHandler.promises.sendEmail('securityAlert', toOld)
|
|
|
|
await EmailHandler.promises.sendEmail('securityAlert', toNew)
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(
|
|
|
|
{ error, userId },
|
|
|
|
'could not send security alert email when primary email changed'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-14 09:54:19 -04:00
|
|
|
async function addEmailAddress(userId, newEmail, affiliationOptions, auditLog) {
|
|
|
|
newEmail = EmailHelper.parseEmail(newEmail)
|
|
|
|
if (!newEmail) {
|
|
|
|
throw new Error('invalid email')
|
|
|
|
}
|
|
|
|
|
|
|
|
await UserGetter.promises.ensureUniqueEmailAddress(newEmail)
|
|
|
|
|
|
|
|
await UserAuditLogHandler.promises.addEntry(
|
|
|
|
userId,
|
|
|
|
'add-email',
|
|
|
|
auditLog.initiatorId,
|
|
|
|
auditLog.ipAddress,
|
|
|
|
{
|
|
|
|
newSecondaryEmail: newEmail
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
try {
|
|
|
|
await InstitutionsAPIPromises.addAffiliation(
|
|
|
|
userId,
|
|
|
|
newEmail,
|
|
|
|
affiliationOptions
|
|
|
|
)
|
|
|
|
} catch (error) {
|
|
|
|
throw OError.tag(error, 'problem adding affiliation while adding email')
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const reversedHostname = newEmail
|
|
|
|
.split('@')[1]
|
|
|
|
.split('')
|
|
|
|
.reverse()
|
|
|
|
.join('')
|
|
|
|
const update = {
|
|
|
|
$push: {
|
|
|
|
emails: { email: newEmail, createdAt: new Date(), reversedHostname }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await UserUpdater.promises.updateUser(userId, update)
|
|
|
|
} catch (error) {
|
|
|
|
throw OError.tag(error, 'problem updating users emails')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-12 10:19:33 -04:00
|
|
|
async function setDefaultEmailAddress(
|
|
|
|
userId,
|
|
|
|
email,
|
|
|
|
allowUnconfirmed,
|
2020-08-12 10:19:55 -04:00
|
|
|
auditLog,
|
|
|
|
sendSecurityAlert
|
2020-08-12 10:19:33 -04:00
|
|
|
) {
|
2020-08-12 10:18:45 -04:00
|
|
|
email = EmailHelper.parseEmail(email)
|
|
|
|
if (email == null) {
|
|
|
|
throw new Error('invalid email')
|
|
|
|
}
|
|
|
|
|
|
|
|
const user = await UserGetter.promises.getUser(userId, {
|
|
|
|
email: 1,
|
|
|
|
emails: 1
|
|
|
|
})
|
|
|
|
if (!user) {
|
|
|
|
throw new Error('invalid userId')
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldEmail = user.email
|
|
|
|
const userEmail = user.emails.find(e => e.email === email)
|
|
|
|
if (!userEmail) {
|
|
|
|
throw new Error('Default email does not belong to user')
|
|
|
|
}
|
|
|
|
if (!userEmail.confirmedAt && !allowUnconfirmed) {
|
|
|
|
throw new Errors.UnconfirmedEmailError()
|
|
|
|
}
|
|
|
|
|
2020-08-12 10:19:33 -04:00
|
|
|
await UserAuditLogHandler.promises.addEntry(
|
|
|
|
userId,
|
|
|
|
'change-primary-email',
|
|
|
|
auditLog.initiatorId,
|
|
|
|
auditLog.ipAddress,
|
|
|
|
{
|
|
|
|
newPrimaryEmail: email,
|
|
|
|
oldPrimaryEmail: oldEmail
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2020-08-12 10:18:45 -04:00
|
|
|
const query = { _id: userId, 'emails.email': email }
|
|
|
|
const update = { $set: { email } }
|
|
|
|
const res = await UserUpdater.promises.updateUser(query, update)
|
|
|
|
|
|
|
|
// this should not happen
|
|
|
|
if (res.n === 0) {
|
|
|
|
throw new Error('email update error')
|
|
|
|
}
|
|
|
|
|
2020-08-12 10:19:55 -04:00
|
|
|
if (sendSecurityAlert) {
|
2020-09-29 10:05:12 -04:00
|
|
|
// no need to wait, errors are logged and not passed back
|
|
|
|
_sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email)
|
2020-08-12 10:19:55 -04:00
|
|
|
}
|
|
|
|
|
2020-08-12 10:18:45 -04:00
|
|
|
try {
|
|
|
|
await NewsletterManager.promises.changeEmail(user, email)
|
|
|
|
} catch (error) {
|
|
|
|
logger.warn(
|
|
|
|
{ err: error, oldEmail, newEmail: email },
|
|
|
|
'Failed to change email in newsletter subscription'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await RecurlyWrapper.promises.updateAccountEmailAddress(user._id, email)
|
|
|
|
} catch (error) {
|
|
|
|
// errors are ignored
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-30 09:57:18 -05:00
|
|
|
async function confirmEmail(userId, email) {
|
|
|
|
// used for initial email confirmation (non-SSO and SSO)
|
|
|
|
// also used for reconfirmation of non-SSO emails
|
|
|
|
const confirmedAt = new Date()
|
|
|
|
email = EmailHelper.parseEmail(email)
|
|
|
|
if (email == null) {
|
|
|
|
throw new Error('invalid email')
|
|
|
|
}
|
|
|
|
logger.log({ userId, email }, 'confirming user email')
|
|
|
|
|
|
|
|
try {
|
|
|
|
await InstitutionsAPIPromises.addAffiliation(userId, email, { confirmedAt })
|
|
|
|
} catch (error) {
|
|
|
|
throw OError.tag(error, 'problem adding affiliation while confirming email')
|
|
|
|
}
|
|
|
|
|
|
|
|
const query = {
|
|
|
|
_id: userId,
|
|
|
|
'emails.email': email
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$set: {
|
|
|
|
'emails.$.reconfirmedAt': confirmedAt
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const user = await UserGetter.promises.getUser(userId)
|
|
|
|
const emailUnconfirmed = user.emails.find(emailData => {
|
|
|
|
if (emailData.email === email && !emailData.confirmedAt) return true
|
|
|
|
})
|
|
|
|
if (emailUnconfirmed) {
|
|
|
|
update.$set['emails.$.confirmedAt'] = confirmedAt
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Features.hasFeature('affiliations')) {
|
|
|
|
update['$unset'] = {
|
|
|
|
'emails.$.affiliationUnchecked': 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const res = await UserUpdater.promises.updateUser(query, update)
|
|
|
|
|
|
|
|
if (res.n === 0) {
|
|
|
|
throw new Errors.NotFoundError('user id and email do no match')
|
|
|
|
}
|
|
|
|
await FeaturesUpdater.promises.refreshFeatures(userId)
|
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
const UserUpdater = {
|
2020-09-01 08:37:09 -04:00
|
|
|
addAffiliationForNewUser(userId, email, affiliationOptions, callback) {
|
|
|
|
if (callback == null) {
|
|
|
|
// affiliationOptions is optional
|
|
|
|
callback = affiliationOptions
|
|
|
|
affiliationOptions = {}
|
|
|
|
}
|
|
|
|
addAffiliation(userId, email, affiliationOptions, error => {
|
2020-02-20 11:08:40 -05:00
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
UserUpdater.updateUser(
|
|
|
|
{ _id: userId, 'emails.email': email },
|
|
|
|
{ $unset: { 'emails.$.affiliationUnchecked': 1 } },
|
2020-08-11 05:35:08 -04:00
|
|
|
error => {
|
|
|
|
if (error) {
|
|
|
|
callback(
|
|
|
|
OError.tag(
|
|
|
|
error,
|
|
|
|
'could not remove affiliationUnchecked flag for user on create',
|
|
|
|
{
|
|
|
|
userId,
|
|
|
|
email
|
|
|
|
}
|
|
|
|
)
|
2020-02-20 11:08:40 -05:00
|
|
|
)
|
2020-08-11 05:35:08 -04:00
|
|
|
} else {
|
|
|
|
callback()
|
2020-02-20 11:08:40 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-05-29 05:21:06 -04:00
|
|
|
updateUser(query, update, callback) {
|
|
|
|
if (callback == null) {
|
2019-08-28 08:59:41 -04:00
|
|
|
callback = () => {}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-10-06 06:31:34 -04:00
|
|
|
|
|
|
|
try {
|
|
|
|
query = normalizeQuery(query)
|
|
|
|
} catch (err) {
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2020-10-01 04:30:26 -04:00
|
|
|
db.users.updateOne(query, update, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
//
|
|
|
|
// DEPRECATED
|
|
|
|
//
|
|
|
|
// Change the user's main email address by adding a new email, switching the
|
|
|
|
// default email and removing the old email. Prefer manipulating multiple
|
|
|
|
// emails and the default rather than calling this method directly
|
|
|
|
//
|
2020-08-12 10:19:33 -04:00
|
|
|
changeEmailAddress(userId, newEmail, auditLog, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
newEmail = EmailHelper.parseEmail(newEmail)
|
|
|
|
if (newEmail == null) {
|
|
|
|
return callback(new Error('invalid email'))
|
|
|
|
}
|
|
|
|
|
|
|
|
let oldEmail = null
|
2019-08-28 08:59:41 -04:00
|
|
|
async.series(
|
2019-05-29 05:21:06 -04:00
|
|
|
[
|
|
|
|
cb =>
|
2019-08-28 08:59:41 -04:00
|
|
|
UserGetter.getUserEmail(userId, (error, email) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
oldEmail = email
|
2019-08-28 08:59:41 -04:00
|
|
|
cb(error)
|
2019-05-29 05:21:06 -04:00
|
|
|
}),
|
2020-09-14 09:54:19 -04:00
|
|
|
cb => UserUpdater.addEmailAddress(userId, newEmail, {}, auditLog, cb),
|
2020-08-12 10:19:33 -04:00
|
|
|
cb =>
|
|
|
|
UserUpdater.setDefaultEmailAddress(
|
|
|
|
userId,
|
|
|
|
newEmail,
|
|
|
|
true,
|
|
|
|
auditLog,
|
2020-08-12 10:19:55 -04:00
|
|
|
true,
|
2020-08-12 10:19:33 -04:00
|
|
|
cb
|
|
|
|
),
|
2019-05-29 05:21:06 -04:00
|
|
|
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
|
2020-09-14 09:54:19 -04:00
|
|
|
addEmailAddress: callbackify(addEmailAddress),
|
2019-05-29 05:21:06 -04:00
|
|
|
|
|
|
|
// remove one of the user's email addresses. The email cannot be the user's
|
|
|
|
// default email address
|
|
|
|
removeEmailAddress(userId, email, callback) {
|
|
|
|
email = EmailHelper.parseEmail(email)
|
|
|
|
if (email == null) {
|
|
|
|
return callback(new Error('invalid email'))
|
|
|
|
}
|
2019-08-28 08:59:41 -04:00
|
|
|
removeAffiliation(userId, email, error => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
2020-08-11 05:35:08 -04:00
|
|
|
OError.tag(error, 'problem removing affiliation')
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
|
|
|
|
const query = { _id: userId, email: { $ne: email } }
|
|
|
|
const update = { $pull: { emails: { email } } }
|
2019-08-28 08:59:41 -04:00
|
|
|
UserUpdater.updateUser(query, update, (error, res) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
2020-08-11 05:35:08 -04:00
|
|
|
OError.tag(error, 'problem removing users email')
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (res.n === 0) {
|
|
|
|
return callback(new Error('Cannot remove email'))
|
|
|
|
}
|
2020-06-04 04:47:02 -04:00
|
|
|
FeaturesUpdater.refreshFeatures(userId, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
// set the default email address by setting the `email` attribute. The email
|
|
|
|
// must be one of the user's multiple emails (`emails` attribute)
|
2020-08-12 10:18:45 -04:00
|
|
|
setDefaultEmailAddress: callbackify(setDefaultEmailAddress),
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-11-30 09:57:18 -05:00
|
|
|
confirmEmail: callbackify(confirmEmail),
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
removeReconfirmFlag(userId, callback) {
|
|
|
|
UserUpdater.updateUser(
|
|
|
|
userId.toString(),
|
2019-05-29 05:21:06 -04:00
|
|
|
{
|
|
|
|
$set: { must_reconfirm: false }
|
|
|
|
},
|
|
|
|
error => callback(error)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
;[
|
|
|
|
'updateUser',
|
|
|
|
'changeEmailAddress',
|
|
|
|
'setDefaultEmailAddress',
|
|
|
|
'addEmailAddress',
|
|
|
|
'removeEmailAddress',
|
|
|
|
'removeReconfirmFlag'
|
|
|
|
].map(method =>
|
|
|
|
metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger)
|
|
|
|
)
|
2019-08-28 08:59:41 -04:00
|
|
|
|
2019-09-30 09:21:31 -04:00
|
|
|
const promises = {
|
2020-02-20 11:08:40 -05:00
|
|
|
addAffiliationForNewUser: promisify(UserUpdater.addAffiliationForNewUser),
|
2020-09-14 09:54:19 -04:00
|
|
|
addEmailAddress,
|
2020-11-30 09:57:18 -05:00
|
|
|
confirmEmail,
|
2020-08-12 10:18:45 -04:00
|
|
|
setDefaultEmailAddress,
|
2020-08-13 09:42:28 -04:00
|
|
|
updateUser: promisify(UserUpdater.updateUser),
|
|
|
|
removeReconfirmFlag: promisify(UserUpdater.removeReconfirmFlag)
|
2019-09-30 09:21:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
UserUpdater.promises = promises
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
module.exports = UserUpdater
|