2021-11-10 08:40:18 -05:00
|
|
|
|
const logger = require('@overleaf/logger')
|
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')
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const { callbackify } = require('util')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
const UserGetter = require('./UserGetter')
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const InstitutionsAPI = 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')
|
2022-02-03 06:37:34 -05:00
|
|
|
|
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
2022-06-08 10:08:11 -04:00
|
|
|
|
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
|
|
|
|
|
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
|
2022-04-14 05:18:15 -04:00
|
|
|
|
const _ = require('lodash')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
|
2020-09-29 10:05:12 -04:00
|
|
|
|
async function _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email) {
|
2022-04-14 05:18:15 -04:00
|
|
|
|
// Send email to the following:
|
|
|
|
|
// - the old primary
|
|
|
|
|
// - the new primary
|
|
|
|
|
// - for all other current (confirmed or recently-enough reconfirmed) email addresses, group by institution if we
|
|
|
|
|
// have it, or domain if we don’t, and for each group send to the most recently reconfirmed (or confirmed if never
|
|
|
|
|
// reconfirmed) address in that group.
|
|
|
|
|
// See #6101.
|
2020-09-29 10:05:12 -04:00
|
|
|
|
const emailOptions = {
|
|
|
|
|
actionDescribed: `the primary email address on your account was changed to ${email}`,
|
2021-04-27 03:52:58 -04:00
|
|
|
|
action: 'change of primary email address',
|
2020-09-29 10:05:12 -04:00
|
|
|
|
}
|
|
|
|
|
|
2022-04-14 05:18:15 -04:00
|
|
|
|
async function sendToRecipients(recipients) {
|
|
|
|
|
// On failure, log the error and carry on so that one email failing does not prevent other emails sending
|
|
|
|
|
for await (const recipient of recipients) {
|
|
|
|
|
try {
|
|
|
|
|
const opts = Object.assign({}, emailOptions, { to: recipient })
|
|
|
|
|
await EmailHandler.promises.sendEmail('securityAlert', opts)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
{ error, userId },
|
|
|
|
|
'could not send security alert email when primary email changed'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-29 10:05:12 -04:00
|
|
|
|
}
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
|
|
|
|
// First, send notification to the old and new primary emails before getting other emails from v1 to ensure that these
|
|
|
|
|
// are still sent in the event of not being able to reach v1
|
|
|
|
|
const oldAndNewPrimaryEmails = [oldEmail, email]
|
|
|
|
|
await sendToRecipients(oldAndNewPrimaryEmails)
|
|
|
|
|
|
|
|
|
|
// Next, get extra recipients with affiliation data
|
|
|
|
|
const emailsData = await UserGetter.promises.getUserFullEmails(userId)
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const extraRecipients = _securityAlertPrimaryEmailChangedExtraRecipients(
|
|
|
|
|
emailsData,
|
|
|
|
|
oldEmail,
|
|
|
|
|
email
|
|
|
|
|
)
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
|
|
|
|
await sendToRecipients(extraRecipients)
|
2020-09-29 10:05:12 -04:00
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
async function addEmailAddress(userId, newEmail, affiliationOptions, auditLog) {
|
|
|
|
|
newEmail = EmailHelper.parseEmail(newEmail)
|
|
|
|
|
if (!newEmail) {
|
|
|
|
|
throw new Error('invalid email')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await UserGetter.promises.ensureUniqueEmailAddress(newEmail)
|
|
|
|
|
|
2022-02-03 06:37:34 -05:00
|
|
|
|
AnalyticsManager.recordEventForUser(userId, 'secondary-email-added')
|
|
|
|
|
|
2020-09-14 09:54:19 -04:00
|
|
|
|
await UserAuditLogHandler.promises.addEntry(
|
|
|
|
|
userId,
|
|
|
|
|
'add-email',
|
|
|
|
|
auditLog.initiatorId,
|
|
|
|
|
auditLog.ipAddress,
|
|
|
|
|
{
|
2021-04-27 03:52:58 -04:00
|
|
|
|
newSecondaryEmail: newEmail,
|
2020-09-14 09:54:19 -04:00
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try {
|
2022-04-27 08:02:40 -04:00
|
|
|
|
await InstitutionsAPI.promises.addAffiliation(
|
2020-09-14 09:54:19 -04:00
|
|
|
|
userId,
|
|
|
|
|
newEmail,
|
|
|
|
|
affiliationOptions
|
|
|
|
|
)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw OError.tag(error, 'problem adding affiliation while adding email')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2021-04-14 09:17:21 -04:00
|
|
|
|
const reversedHostname = newEmail.split('@')[1].split('').reverse().join('')
|
2020-09-14 09:54:19 -04:00
|
|
|
|
const update = {
|
|
|
|
|
$push: {
|
2021-04-27 03:52:58 -04:00
|
|
|
|
emails: { email: newEmail, createdAt: new Date(), reversedHostname },
|
|
|
|
|
},
|
2020-09-14 09:54:19 -04:00
|
|
|
|
}
|
2022-04-27 08:02:40 -04:00
|
|
|
|
await updateUser(userId, update)
|
2020-09-14 09:54:19 -04:00
|
|
|
|
} catch (error) {
|
|
|
|
|
throw OError.tag(error, 'problem updating users emails')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-20 05:19:01 -04:00
|
|
|
|
async function clearSAMLData(userId, auditLog, sendEmail) {
|
|
|
|
|
const user = await UserGetter.promises.getUser(userId, {
|
|
|
|
|
email: 1,
|
|
|
|
|
emails: 1,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await UserAuditLogHandler.promises.addEntry(
|
|
|
|
|
userId,
|
|
|
|
|
'clear-institution-sso-data',
|
|
|
|
|
auditLog.initiatorId,
|
|
|
|
|
auditLog.ipAddress,
|
|
|
|
|
{}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const update = {
|
|
|
|
|
$unset: {
|
|
|
|
|
samlIdentifiers: 1,
|
|
|
|
|
'emails.$[].samlProviderId': 1,
|
|
|
|
|
},
|
|
|
|
|
}
|
2022-04-27 08:02:40 -04:00
|
|
|
|
await updateUser(userId, update)
|
2021-05-20 05:19:01 -04:00
|
|
|
|
|
|
|
|
|
for (const emailData of user.emails) {
|
2022-04-27 08:02:40 -04:00
|
|
|
|
await InstitutionsAPI.promises.removeEntitlement(userId, emailData.email)
|
2021-05-20 05:19:01 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await FeaturesUpdater.promises.refreshFeatures(
|
|
|
|
|
userId,
|
|
|
|
|
'clear-institution-sso-data'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (sendEmail) {
|
|
|
|
|
await EmailHandler.promises.sendEmail('SAMLDataCleared', { to: user.email })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 08:02:40 -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: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,
|
2021-04-27 03:52:58 -04:00
|
|
|
|
emails: 1,
|
2020-08-12 10:18:45 -04:00
|
|
|
|
})
|
|
|
|
|
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,
|
2021-04-27 03:52:58 -04:00
|
|
|
|
oldPrimaryEmail: oldEmail,
|
2020-08-12 10:19:33 -04:00
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2020-08-12 10:18:45 -04:00
|
|
|
|
const query = { _id: userId, 'emails.email': email }
|
2022-02-03 06:37:34 -05:00
|
|
|
|
const update = { $set: { email, lastPrimaryEmailCheck: new Date() } }
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const res = await updateUser(query, update)
|
2020-08-12 10:18:45 -04:00
|
|
|
|
|
|
|
|
|
// this should not happen
|
2021-10-28 04:35:02 -04:00
|
|
|
|
if (res.matchedCount !== 1) {
|
2020-08-12 10:18:45 -04:00
|
|
|
|
throw new Error('email update error')
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-03 06:37:34 -05:00
|
|
|
|
AnalyticsManager.recordEventForUser(userId, 'primary-email-address-updated')
|
|
|
|
|
|
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
|
2022-04-27 08:02:40 -04:00
|
|
|
|
_sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email).catch(
|
|
|
|
|
err => {
|
|
|
|
|
logger.error({ err }, 'failed to send security alert 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-08 10:08:11 -04:00
|
|
|
|
async function confirmEmail(userId, email, affiliationOptions) {
|
2020-11-30 09:57:18 -05:00
|
|
|
|
// 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')
|
|
|
|
|
}
|
2022-05-16 08:38:18 -04:00
|
|
|
|
logger.debug({ userId, email }, 'confirming user email')
|
2020-11-30 09:57:18 -05:00
|
|
|
|
|
|
|
|
|
try {
|
2022-06-08 10:08:11 -04:00
|
|
|
|
affiliationOptions = affiliationOptions || {}
|
|
|
|
|
affiliationOptions.confirmedAt = confirmedAt
|
|
|
|
|
await InstitutionsAPI.promises.addAffiliation(
|
|
|
|
|
userId,
|
|
|
|
|
email,
|
|
|
|
|
affiliationOptions
|
|
|
|
|
)
|
2020-11-30 09:57:18 -05:00
|
|
|
|
} catch (error) {
|
|
|
|
|
throw OError.tag(error, 'problem adding affiliation while confirming email')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = {
|
|
|
|
|
_id: userId,
|
2021-04-27 03:52:58 -04:00
|
|
|
|
'emails.email': email,
|
2020-11-30 09:57:18 -05:00
|
|
|
|
}
|
2021-01-07 09:21:54 -05:00
|
|
|
|
|
|
|
|
|
// only update confirmedAt if it was not previously set
|
2020-11-30 09:57:18 -05:00
|
|
|
|
const update = {
|
|
|
|
|
$set: {
|
2021-04-27 03:52:58 -04:00
|
|
|
|
'emails.$.reconfirmedAt': confirmedAt,
|
2021-01-07 09:21:54 -05:00
|
|
|
|
},
|
|
|
|
|
$min: {
|
2021-04-27 03:52:58 -04:00
|
|
|
|
'emails.$.confirmedAt': confirmedAt,
|
|
|
|
|
},
|
2020-11-30 09:57:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Features.hasFeature('affiliations')) {
|
2020-12-16 05:37:00 -05:00
|
|
|
|
update.$unset = {
|
2021-04-27 03:52:58 -04:00
|
|
|
|
'emails.$.affiliationUnchecked': 1,
|
2020-11-30 09:57:18 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const res = await updateUser(query, update)
|
2020-11-30 09:57:18 -05:00
|
|
|
|
|
2021-10-28 04:35:02 -04:00
|
|
|
|
if (res.matchedCount !== 1) {
|
2020-11-30 09:57:18 -05:00
|
|
|
|
throw new Errors.NotFoundError('user id and email do no match')
|
|
|
|
|
}
|
2021-05-11 10:08:37 -04:00
|
|
|
|
await FeaturesUpdater.promises.refreshFeatures(userId, 'confirm-email')
|
2022-06-08 10:08:11 -04:00
|
|
|
|
try {
|
|
|
|
|
await maybeCreateRedundantSubscriptionNotification(userId, email)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.err(
|
|
|
|
|
{ err: error },
|
|
|
|
|
'error checking redundant subscription on email confirmation'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function maybeCreateRedundantSubscriptionNotification(userId, email) {
|
|
|
|
|
const subscription =
|
|
|
|
|
await SubscriptionLocator.promises.getUserIndividualSubscription(userId)
|
|
|
|
|
if (!subscription || subscription.groupPlan) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const affiliations = await InstitutionsAPI.promises.getUserAffiliations(
|
|
|
|
|
userId
|
|
|
|
|
)
|
|
|
|
|
const confirmedAffiliation = affiliations.find(a => a.email === email)
|
2022-06-14 04:14:10 -04:00
|
|
|
|
if (!confirmedAffiliation || confirmedAffiliation.licence === 'free') {
|
2022-06-08 10:08:11 -04:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await NotificationsBuilder.promises
|
|
|
|
|
.redundantPersonalSubscription(
|
|
|
|
|
{
|
|
|
|
|
institutionId: confirmedAffiliation.institution.id,
|
|
|
|
|
institutionName: confirmedAffiliation.institution.name,
|
|
|
|
|
},
|
|
|
|
|
{ _id: userId }
|
|
|
|
|
)
|
|
|
|
|
.create()
|
2020-11-30 09:57:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-27 04:25:50 -04:00
|
|
|
|
async function removeEmailAddress(
|
|
|
|
|
userId,
|
|
|
|
|
email,
|
|
|
|
|
auditLog,
|
|
|
|
|
skipParseEmail = false
|
|
|
|
|
) {
|
2022-01-12 11:19:30 -05:00
|
|
|
|
// remove one of the user's email addresses. The email cannot be the user's
|
|
|
|
|
// default email address
|
|
|
|
|
if (!skipParseEmail) {
|
|
|
|
|
email = EmailHelper.parseEmail(email)
|
|
|
|
|
} else if (skipParseEmail && typeof email !== 'string') {
|
|
|
|
|
throw new Error('email must be a string')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!email) {
|
|
|
|
|
throw new Error('invalid email')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isMainEmail = await UserGetter.promises.getUserByMainEmail(email, {
|
|
|
|
|
_id: 1,
|
|
|
|
|
})
|
|
|
|
|
if (isMainEmail) {
|
|
|
|
|
throw new Error('cannot remove primary email')
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-27 04:25:50 -04:00
|
|
|
|
await UserAuditLogHandler.promises.addEntry(
|
|
|
|
|
userId,
|
|
|
|
|
'remove-email',
|
|
|
|
|
auditLog.initiatorId,
|
|
|
|
|
auditLog.ipAddress,
|
|
|
|
|
{
|
|
|
|
|
removedEmail: email,
|
|
|
|
|
// Add optional extra info
|
|
|
|
|
...(auditLog.extraInfo || {}),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2022-01-12 11:19:30 -05:00
|
|
|
|
try {
|
2022-04-27 08:02:40 -04:00
|
|
|
|
await InstitutionsAPI.promises.removeAffiliation(userId, email)
|
2022-01-12 11:19:30 -05:00
|
|
|
|
} catch (error) {
|
|
|
|
|
OError.tag(error, 'problem removing affiliation')
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = { _id: userId, email: { $ne: email } }
|
|
|
|
|
const update = { $pull: { emails: { email } } }
|
|
|
|
|
|
|
|
|
|
let res
|
|
|
|
|
try {
|
2022-04-27 08:02:40 -04:00
|
|
|
|
res = await updateUser(query, update)
|
2022-01-12 11:19:30 -05:00
|
|
|
|
} catch (error) {
|
|
|
|
|
OError.tag(error, 'problem removing users email')
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (res.matchedCount !== 1) {
|
|
|
|
|
throw new Error('Cannot remove email')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await FeaturesUpdater.promises.refreshFeatures(userId, 'remove-email')
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
async function addAffiliationForNewUser(
|
|
|
|
|
userId,
|
|
|
|
|
email,
|
|
|
|
|
affiliationOptions = {}
|
|
|
|
|
) {
|
|
|
|
|
await InstitutionsAPI.promises.addAffiliation(
|
|
|
|
|
userId,
|
|
|
|
|
email,
|
|
|
|
|
affiliationOptions
|
|
|
|
|
)
|
|
|
|
|
try {
|
|
|
|
|
await updateUser(
|
|
|
|
|
{ _id: userId, 'emails.email': email },
|
|
|
|
|
{ $unset: { 'emails.$.affiliationUnchecked': 1 } }
|
|
|
|
|
)
|
|
|
|
|
} catch (error) {
|
2022-06-16 09:17:31 -04:00
|
|
|
|
logger.error(
|
|
|
|
|
OError.tag(
|
|
|
|
|
error,
|
|
|
|
|
'could not remove affiliationUnchecked flag for user on create',
|
|
|
|
|
{
|
|
|
|
|
userId,
|
|
|
|
|
email,
|
|
|
|
|
}
|
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
)
|
2022-04-27 08:02:40 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
async function updateUser(query, update) {
|
|
|
|
|
query = normalizeQuery(query)
|
|
|
|
|
const result = await db.users.updateOne(query, update)
|
|
|
|
|
return result
|
|
|
|
|
}
|
2021-05-20 05:19:01 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -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
|
|
|
|
|
*/
|
|
|
|
|
async function changeEmailAddress(userId, newEmail, auditLog) {
|
|
|
|
|
newEmail = EmailHelper.parseEmail(newEmail)
|
|
|
|
|
if (newEmail == null) {
|
|
|
|
|
throw new Error('invalid email')
|
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const oldEmail = await UserGetter.promises.getUserEmail(userId)
|
|
|
|
|
await addEmailAddress(userId, newEmail, {}, auditLog)
|
|
|
|
|
await setDefaultEmailAddress(userId, newEmail, true, auditLog, true)
|
2022-07-27 04:25:50 -04:00
|
|
|
|
await removeEmailAddress(userId, oldEmail, auditLog)
|
2022-04-27 08:02:40 -04:00
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
async function removeReconfirmFlag(userId) {
|
|
|
|
|
await updateUser(userId.toString(), { $set: { must_reconfirm: false } })
|
|
|
|
|
}
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
function _securityAlertPrimaryEmailChangedExtraRecipients(
|
|
|
|
|
emailsData,
|
|
|
|
|
oldEmail,
|
|
|
|
|
email
|
|
|
|
|
) {
|
|
|
|
|
// Group by institution if we have it, or domain if we don’t, and for each group send to the most recently
|
|
|
|
|
// reconfirmed (or confirmed if never reconfirmed) address in that group. We also remove the original and new
|
|
|
|
|
// primary email addresses because they are emailed separately
|
|
|
|
|
// See #6101.
|
|
|
|
|
function sortEmailsByConfirmation(emails) {
|
|
|
|
|
return emails.sort((e1, e2) => e2.lastConfirmedAt - e1.lastConfirmedAt)
|
|
|
|
|
}
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
const recipients = new Set()
|
|
|
|
|
const emailsToIgnore = new Set([oldEmail, email])
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
// Remove non-confirmed emails
|
|
|
|
|
const confirmedEmails = emailsData.filter(email => !!email.lastConfirmedAt)
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
// Group other emails by institution, separating out those with no institution and grouping them instead by domain.
|
|
|
|
|
// The keys for each group are not used for anything other than the grouping, so can have a slightly paranoid format
|
|
|
|
|
// to avoid any potential clash
|
|
|
|
|
const groupedEmails = _.groupBy(confirmedEmails, emailData => {
|
|
|
|
|
if (!emailData.affiliation || !emailData.affiliation.institution) {
|
|
|
|
|
return `domain:${EmailHelper.getDomain(emailData.email)}`
|
|
|
|
|
}
|
|
|
|
|
return `institution_id:${emailData.affiliation.institution.id}`
|
|
|
|
|
})
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
// For each group of emails, order the emails by (re-)confirmation date and pick the first
|
|
|
|
|
for (const emails of Object.values(groupedEmails)) {
|
|
|
|
|
// Sort by confirmation and pick the first
|
|
|
|
|
sortEmailsByConfirmation(emails)
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
// Ignore original and new primary email addresses
|
|
|
|
|
const recipient = emails[0].email
|
|
|
|
|
if (!emailsToIgnore.has(recipient)) {
|
|
|
|
|
recipients.add(emails[0].email)
|
2022-04-14 05:18:15 -04:00
|
|
|
|
}
|
2022-04-27 08:02:40 -04:00
|
|
|
|
}
|
2022-04-14 05:18:15 -04:00
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
return Array.from(recipients)
|
2019-09-30 09:21:31 -04:00
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 08:02:40 -04:00
|
|
|
|
module.exports = {
|
|
|
|
|
addAffiliationForNewUser: callbackify(addAffiliationForNewUser),
|
|
|
|
|
addEmailAddress: callbackify(addEmailAddress),
|
|
|
|
|
changeEmailAddress: callbackify(changeEmailAddress),
|
|
|
|
|
clearSAMLData: callbackify(clearSAMLData),
|
|
|
|
|
confirmEmail: callbackify(confirmEmail),
|
|
|
|
|
removeEmailAddress: callbackify(removeEmailAddress),
|
|
|
|
|
removeReconfirmFlag: callbackify(removeReconfirmFlag),
|
|
|
|
|
setDefaultEmailAddress: callbackify(setDefaultEmailAddress),
|
|
|
|
|
updateUser: callbackify(updateUser),
|
|
|
|
|
promises: {
|
|
|
|
|
addAffiliationForNewUser,
|
|
|
|
|
addEmailAddress,
|
|
|
|
|
changeEmailAddress,
|
|
|
|
|
clearSAMLData,
|
|
|
|
|
confirmEmail,
|
|
|
|
|
removeEmailAddress,
|
|
|
|
|
removeReconfirmFlag,
|
|
|
|
|
setDefaultEmailAddress,
|
|
|
|
|
updateUser,
|
|
|
|
|
},
|
|
|
|
|
}
|