UserUpdater.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. const logger = require('logger-sharelatex')
  2. const OError = require('@overleaf/o-error')
  3. const { db } = require('../../infrastructure/mongodb')
  4. const { normalizeQuery } = require('../Helpers/Mongo')
  5. const metrics = require('@overleaf/metrics')
  6. const async = require('async')
  7. const { callbackify, promisify } = require('util')
  8. const UserGetter = require('./UserGetter')
  9. const {
  10. addAffiliation,
  11. removeAffiliation,
  12. promises: InstitutionsAPIPromises,
  13. } = require('../Institutions/InstitutionsAPI')
  14. const Features = require('../../infrastructure/Features')
  15. const FeaturesUpdater = require('../Subscription/FeaturesUpdater')
  16. const EmailHandler = require('../Email/EmailHandler')
  17. const EmailHelper = require('../Helpers/EmailHelper')
  18. const Errors = require('../Errors/Errors')
  19. const NewsletterManager = require('../Newsletter/NewsletterManager')
  20. const RecurlyWrapper = require('../Subscription/RecurlyWrapper')
  21. const UserAuditLogHandler = require('./UserAuditLogHandler')
  22. async function _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email) {
  23. // send email to both old and new primary email
  24. const emailOptions = {
  25. actionDescribed: `the primary email address on your account was changed to ${email}`,
  26. action: 'change of primary email address',
  27. }
  28. const toOld = Object.assign({}, emailOptions, { to: oldEmail })
  29. const toNew = Object.assign({}, emailOptions, { to: email })
  30. try {
  31. await EmailHandler.promises.sendEmail('securityAlert', toOld)
  32. await EmailHandler.promises.sendEmail('securityAlert', toNew)
  33. } catch (error) {
  34. logger.error(
  35. { error, userId },
  36. 'could not send security alert email when primary email changed'
  37. )
  38. }
  39. }
  40. async function addEmailAddress(userId, newEmail, affiliationOptions, auditLog) {
  41. newEmail = EmailHelper.parseEmail(newEmail)
  42. if (!newEmail) {
  43. throw new Error('invalid email')
  44. }
  45. await UserGetter.promises.ensureUniqueEmailAddress(newEmail)
  46. await UserAuditLogHandler.promises.addEntry(
  47. userId,
  48. 'add-email',
  49. auditLog.initiatorId,
  50. auditLog.ipAddress,
  51. {
  52. newSecondaryEmail: newEmail,
  53. }
  54. )
  55. try {
  56. await InstitutionsAPIPromises.addAffiliation(
  57. userId,
  58. newEmail,
  59. affiliationOptions
  60. )
  61. } catch (error) {
  62. throw OError.tag(error, 'problem adding affiliation while adding email')
  63. }
  64. try {
  65. const reversedHostname = newEmail.split('@')[1].split('').reverse().join('')
  66. const update = {
  67. $push: {
  68. emails: { email: newEmail, createdAt: new Date(), reversedHostname },
  69. },
  70. }
  71. await UserUpdater.promises.updateUser(userId, update)
  72. } catch (error) {
  73. throw OError.tag(error, 'problem updating users emails')
  74. }
  75. }
  76. async function clearSAMLData(userId, auditLog, sendEmail) {
  77. const user = await UserGetter.promises.getUser(userId, {
  78. email: 1,
  79. emails: 1,
  80. })
  81. await UserAuditLogHandler.promises.addEntry(
  82. userId,
  83. 'clear-institution-sso-data',
  84. auditLog.initiatorId,
  85. auditLog.ipAddress,
  86. {}
  87. )
  88. const update = {
  89. $unset: {
  90. samlIdentifiers: 1,
  91. 'emails.$[].samlProviderId': 1,
  92. },
  93. }
  94. await UserUpdater.promises.updateUser(userId, update)
  95. for (const emailData of user.emails) {
  96. await InstitutionsAPIPromises.removeEntitlement(userId, emailData.email)
  97. }
  98. await FeaturesUpdater.promises.refreshFeatures(
  99. userId,
  100. 'clear-institution-sso-data'
  101. )
  102. if (sendEmail) {
  103. await EmailHandler.promises.sendEmail('SAMLDataCleared', { to: user.email })
  104. }
  105. }
  106. async function setDefaultEmailAddress(
  107. userId,
  108. email,
  109. allowUnconfirmed,
  110. auditLog,
  111. sendSecurityAlert
  112. ) {
  113. email = EmailHelper.parseEmail(email)
  114. if (email == null) {
  115. throw new Error('invalid email')
  116. }
  117. const user = await UserGetter.promises.getUser(userId, {
  118. email: 1,
  119. emails: 1,
  120. })
  121. if (!user) {
  122. throw new Error('invalid userId')
  123. }
  124. const oldEmail = user.email
  125. const userEmail = user.emails.find(e => e.email === email)
  126. if (!userEmail) {
  127. throw new Error('Default email does not belong to user')
  128. }
  129. if (!userEmail.confirmedAt && !allowUnconfirmed) {
  130. throw new Errors.UnconfirmedEmailError()
  131. }
  132. await UserAuditLogHandler.promises.addEntry(
  133. userId,
  134. 'change-primary-email',
  135. auditLog.initiatorId,
  136. auditLog.ipAddress,
  137. {
  138. newPrimaryEmail: email,
  139. oldPrimaryEmail: oldEmail,
  140. }
  141. )
  142. const query = { _id: userId, 'emails.email': email }
  143. const update = { $set: { email } }
  144. const res = await UserUpdater.promises.updateUser(query, update)
  145. // this should not happen
  146. if (res.n === 0) {
  147. throw new Error('email update error')
  148. }
  149. if (sendSecurityAlert) {
  150. // no need to wait, errors are logged and not passed back
  151. _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email)
  152. }
  153. try {
  154. await NewsletterManager.promises.changeEmail(user, email)
  155. } catch (error) {
  156. logger.warn(
  157. { err: error, oldEmail, newEmail: email },
  158. 'Failed to change email in newsletter subscription'
  159. )
  160. }
  161. try {
  162. await RecurlyWrapper.promises.updateAccountEmailAddress(user._id, email)
  163. } catch (error) {
  164. // errors are ignored
  165. }
  166. }
  167. async function confirmEmail(userId, email) {
  168. // used for initial email confirmation (non-SSO and SSO)
  169. // also used for reconfirmation of non-SSO emails
  170. const confirmedAt = new Date()
  171. email = EmailHelper.parseEmail(email)
  172. if (email == null) {
  173. throw new Error('invalid email')
  174. }
  175. logger.log({ userId, email }, 'confirming user email')
  176. try {
  177. await InstitutionsAPIPromises.addAffiliation(userId, email, { confirmedAt })
  178. } catch (error) {
  179. throw OError.tag(error, 'problem adding affiliation while confirming email')
  180. }
  181. const query = {
  182. _id: userId,
  183. 'emails.email': email,
  184. }
  185. // only update confirmedAt if it was not previously set
  186. const update = {
  187. $set: {
  188. 'emails.$.reconfirmedAt': confirmedAt,
  189. },
  190. $min: {
  191. 'emails.$.confirmedAt': confirmedAt,
  192. },
  193. }
  194. if (Features.hasFeature('affiliations')) {
  195. update.$unset = {
  196. 'emails.$.affiliationUnchecked': 1,
  197. }
  198. }
  199. const res = await UserUpdater.promises.updateUser(query, update)
  200. if (res.n === 0) {
  201. throw new Errors.NotFoundError('user id and email do no match')
  202. }
  203. await FeaturesUpdater.promises.refreshFeatures(userId, 'confirm-email')
  204. }
  205. const UserUpdater = {
  206. addAffiliationForNewUser(userId, email, affiliationOptions, callback) {
  207. if (callback == null) {
  208. // affiliationOptions is optional
  209. callback = affiliationOptions
  210. affiliationOptions = {}
  211. }
  212. addAffiliation(userId, email, affiliationOptions, error => {
  213. if (error) {
  214. return callback(error)
  215. }
  216. UserUpdater.updateUser(
  217. { _id: userId, 'emails.email': email },
  218. { $unset: { 'emails.$.affiliationUnchecked': 1 } },
  219. error => {
  220. if (error) {
  221. callback(
  222. OError.tag(
  223. error,
  224. 'could not remove affiliationUnchecked flag for user on create',
  225. {
  226. userId,
  227. email,
  228. }
  229. )
  230. )
  231. } else {
  232. callback()
  233. }
  234. }
  235. )
  236. })
  237. },
  238. updateUser(query, update, callback) {
  239. if (callback == null) {
  240. callback = () => {}
  241. }
  242. try {
  243. query = normalizeQuery(query)
  244. } catch (err) {
  245. return callback(err)
  246. }
  247. db.users.updateOne(query, update, callback)
  248. },
  249. //
  250. // DEPRECATED
  251. //
  252. // Change the user's main email address by adding a new email, switching the
  253. // default email and removing the old email. Prefer manipulating multiple
  254. // emails and the default rather than calling this method directly
  255. //
  256. changeEmailAddress(userId, newEmail, auditLog, callback) {
  257. newEmail = EmailHelper.parseEmail(newEmail)
  258. if (newEmail == null) {
  259. return callback(new Error('invalid email'))
  260. }
  261. let oldEmail = null
  262. async.series(
  263. [
  264. cb =>
  265. UserGetter.getUserEmail(userId, (error, email) => {
  266. oldEmail = email
  267. cb(error)
  268. }),
  269. cb => UserUpdater.addEmailAddress(userId, newEmail, {}, auditLog, cb),
  270. cb =>
  271. UserUpdater.setDefaultEmailAddress(
  272. userId,
  273. newEmail,
  274. true,
  275. auditLog,
  276. true,
  277. cb
  278. ),
  279. cb => UserUpdater.removeEmailAddress(userId, oldEmail, cb),
  280. ],
  281. callback
  282. )
  283. },
  284. // Add a new email address for the user. Email cannot be already used by this
  285. // or any other user
  286. addEmailAddress: callbackify(addEmailAddress),
  287. // remove one of the user's email addresses. The email cannot be the user's
  288. // default email address
  289. removeEmailAddress(userId, email, callback) {
  290. email = EmailHelper.parseEmail(email)
  291. if (email == null) {
  292. return callback(new Error('invalid email'))
  293. }
  294. removeAffiliation(userId, email, error => {
  295. if (error != null) {
  296. OError.tag(error, 'problem removing affiliation')
  297. return callback(error)
  298. }
  299. const query = { _id: userId, email: { $ne: email } }
  300. const update = { $pull: { emails: { email } } }
  301. UserUpdater.updateUser(query, update, (error, res) => {
  302. if (error != null) {
  303. OError.tag(error, 'problem removing users email')
  304. return callback(error)
  305. }
  306. if (res.n === 0) {
  307. return callback(new Error('Cannot remove email'))
  308. }
  309. FeaturesUpdater.refreshFeatures(userId, 'remove-email', callback)
  310. })
  311. })
  312. },
  313. clearSAMLData: callbackify(clearSAMLData),
  314. // set the default email address by setting the `email` attribute. The email
  315. // must be one of the user's multiple emails (`emails` attribute)
  316. setDefaultEmailAddress: callbackify(setDefaultEmailAddress),
  317. confirmEmail: callbackify(confirmEmail),
  318. removeReconfirmFlag(userId, callback) {
  319. UserUpdater.updateUser(
  320. userId.toString(),
  321. {
  322. $set: { must_reconfirm: false },
  323. },
  324. error => callback(error)
  325. )
  326. },
  327. }
  328. ;[
  329. 'updateUser',
  330. 'changeEmailAddress',
  331. 'setDefaultEmailAddress',
  332. 'addEmailAddress',
  333. 'removeEmailAddress',
  334. 'removeReconfirmFlag',
  335. ].map(method =>
  336. metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger)
  337. )
  338. const promises = {
  339. addAffiliationForNewUser: promisify(UserUpdater.addAffiliationForNewUser),
  340. addEmailAddress,
  341. confirmEmail,
  342. setDefaultEmailAddress,
  343. updateUser: promisify(UserUpdater.updateUser),
  344. removeReconfirmFlag: promisify(UserUpdater.removeReconfirmFlag),
  345. }
  346. UserUpdater.promises = promises
  347. module.exports = UserUpdater