UserEmailsController.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. const AuthenticationController = require('../Authentication/AuthenticationController')
  2. const Settings = require('@overleaf/settings')
  3. const logger = require('@overleaf/logger')
  4. const SessionManager = require('../Authentication/SessionManager')
  5. const UserGetter = require('./UserGetter')
  6. const UserUpdater = require('./UserUpdater')
  7. const UserSessionsManager = require('./UserSessionsManager')
  8. const EmailHandler = require('../Email/EmailHandler')
  9. const EmailHelper = require('../Helpers/EmailHelper')
  10. const UserEmailsConfirmationHandler = require('./UserEmailsConfirmationHandler')
  11. const { endorseAffiliation } = require('../Institutions/InstitutionsAPI')
  12. const Errors = require('../Errors/Errors')
  13. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  14. const { expressify } = require('@overleaf/promise-utils')
  15. const AsyncFormHelper = require('../Helpers/AsyncFormHelper')
  16. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  17. const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
  18. const UserAuditLogHandler = require('./UserAuditLogHandler')
  19. const { RateLimiter } = require('../../infrastructure/RateLimiter')
  20. const Features = require('../../infrastructure/Features')
  21. const tsscmp = require('tsscmp')
  22. const Modules = require('../../infrastructure/Modules')
  23. const AUDIT_LOG_TOKEN_PREFIX_LENGTH = 10
  24. const sendSecondaryConfirmCodeRateLimiter = new RateLimiter(
  25. 'send-secondary-confirmation-code',
  26. {
  27. points: 1,
  28. duration: 60,
  29. }
  30. )
  31. const checkSecondaryConfirmCodeRateLimiter = new RateLimiter(
  32. 'check-secondary-confirmation-code-per-email',
  33. {
  34. points: 10,
  35. duration: 60,
  36. }
  37. )
  38. const resendSecondaryConfirmCodeRateLimiter = new RateLimiter(
  39. 'resend-secondary-confirmation-code',
  40. {
  41. points: 1,
  42. duration: 60,
  43. }
  44. )
  45. async function _sendSecurityAlertEmail(user, email) {
  46. const emailOptions = {
  47. to: user.email,
  48. actionDescribed: `a secondary email address has been added to your account ${user.email}`,
  49. message: [
  50. `<span style="display:inline-block;padding: 0 20px;width:100%;">Added: <br/><b>${email}</b></span>`,
  51. ],
  52. action: 'secondary email address added',
  53. }
  54. await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
  55. }
  56. /**
  57. * This method is for adding a secondary email to be confirmed via an emailed link.
  58. * For code confirmation, see the `addWithConfirmationCode` method in this file.
  59. */
  60. async function add(req, res, next) {
  61. const userId = SessionManager.getLoggedInUserId(req.session)
  62. const email = EmailHelper.parseEmail(req.body.email)
  63. if (!email) {
  64. return res.sendStatus(422)
  65. }
  66. const user = await UserGetter.promises.getUser(userId, {
  67. email: 1,
  68. 'emails.email': 1,
  69. })
  70. if (user.emails.length >= Settings.emailAddressLimit) {
  71. return res.status(422).json({ message: 'secondary email limit exceeded' })
  72. }
  73. const affiliationOptions = {
  74. university: req.body.university,
  75. role: req.body.role,
  76. department: req.body.department,
  77. }
  78. try {
  79. await UserUpdater.promises.addEmailAddress(
  80. userId,
  81. email,
  82. affiliationOptions,
  83. {
  84. initiatorId: user._id,
  85. ipAddress: req.ip,
  86. }
  87. )
  88. } catch (error) {
  89. return UserEmailsController._handleEmailError(error, req, res, next)
  90. }
  91. await _sendSecurityAlertEmail(user, email)
  92. await UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
  93. userId,
  94. email
  95. )
  96. res.sendStatus(204)
  97. }
  98. function resendConfirmation(req, res, next) {
  99. const userId = SessionManager.getLoggedInUserId(req.session)
  100. const email = EmailHelper.parseEmail(req.body.email)
  101. if (!email) {
  102. return res.sendStatus(422)
  103. }
  104. UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (error, user) {
  105. if (error) {
  106. return next(error)
  107. }
  108. if (!user || user._id.toString() !== userId) {
  109. return res.sendStatus(422)
  110. }
  111. UserEmailsConfirmationHandler.sendConfirmationEmail(
  112. userId,
  113. email,
  114. function (error) {
  115. if (error) {
  116. return next(error)
  117. }
  118. res.sendStatus(200)
  119. }
  120. )
  121. })
  122. }
  123. function sendReconfirmation(req, res, next) {
  124. const userId = SessionManager.getLoggedInUserId(req.session)
  125. const email = EmailHelper.parseEmail(req.body.email)
  126. if (!email) {
  127. return res.sendStatus(400)
  128. }
  129. UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (error, user) {
  130. if (error) {
  131. return next(error)
  132. }
  133. if (!user || user._id.toString() !== userId) {
  134. return res.sendStatus(422)
  135. }
  136. UserEmailsConfirmationHandler.sendReconfirmationEmail(
  137. userId,
  138. email,
  139. function (error) {
  140. if (error) {
  141. return next(error)
  142. }
  143. res.sendStatus(204)
  144. }
  145. )
  146. })
  147. }
  148. /**
  149. * This method is for adding a secondary email to be confirmed via a code.
  150. * For email link confirmation see the `add` method in this file.
  151. */
  152. async function addWithConfirmationCode(req, res) {
  153. delete req.session.pendingSecondaryEmail
  154. const userId = SessionManager.getLoggedInUserId(req.session)
  155. const email = EmailHelper.parseEmail(req.body.email)
  156. if (!email) {
  157. return res.sendStatus(422)
  158. }
  159. const user = await UserGetter.promises.getUser(userId, {
  160. email: 1,
  161. 'emails.email': 1,
  162. })
  163. if (user.emails.length >= Settings.emailAddressLimit) {
  164. return res.status(422).json({ message: 'secondary email limit exceeded' })
  165. }
  166. try {
  167. await UserGetter.promises.ensureUniqueEmailAddress(email)
  168. await sendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
  169. method: 'email',
  170. })
  171. await UserAuditLogHandler.promises.addEntry(
  172. userId,
  173. 'request-add-email-code',
  174. userId,
  175. req.ip,
  176. {
  177. newSecondaryEmail: email,
  178. }
  179. )
  180. const { confirmCode, confirmCodeExpiresTimestamp } =
  181. await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
  182. email,
  183. true
  184. )
  185. req.session.pendingSecondaryEmail = {
  186. email,
  187. confirmCode,
  188. confirmCodeExpiresTimestamp,
  189. }
  190. return res.sendStatus(200)
  191. } catch (err) {
  192. if (err.name === 'EmailExistsError') {
  193. return res.status(409).json({
  194. message: {
  195. type: 'error',
  196. text: req.i18n.translate('email_already_registered'),
  197. },
  198. })
  199. }
  200. if (err?.remainingPoints === 0) {
  201. return res.status(429).json({})
  202. }
  203. logger.err({ err }, 'failed to send confirmation code')
  204. delete req.session.pendingSecondaryEmail
  205. return res.status(500).json({
  206. message: {
  207. key: 'error_performing_request',
  208. },
  209. })
  210. }
  211. }
  212. async function checkSecondaryEmailConfirmationCode(req, res) {
  213. const userId = SessionManager.getLoggedInUserId(req.session)
  214. const code = req.body.code
  215. const user = await UserGetter.promises.getUser(userId, {
  216. email: 1,
  217. 'emails.email': 1,
  218. })
  219. if (!req.session.pendingSecondaryEmail) {
  220. logger.err(
  221. {},
  222. 'error checking confirmation code. missing pendingSecondaryEmail'
  223. )
  224. return res.status(500).json({
  225. message: {
  226. key: 'error_performing_request',
  227. },
  228. })
  229. }
  230. try {
  231. await checkSecondaryConfirmCodeRateLimiter.consume(
  232. req.session.pendingSecondaryEmail.email,
  233. 1,
  234. { method: 'email' }
  235. )
  236. } catch (err) {
  237. if (err?.remainingPoints === 0) {
  238. return res.sendStatus(429)
  239. } else {
  240. return res.status(500).json({
  241. message: {
  242. key: 'error_performing_request',
  243. },
  244. })
  245. }
  246. }
  247. if (
  248. req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp < Date.now()
  249. ) {
  250. return res.status(403).json({
  251. message: { key: 'expired_confirmation_code' },
  252. })
  253. }
  254. if (!tsscmp(req.session.pendingSecondaryEmail.confirmCode, code)) {
  255. return res.status(403).json({
  256. message: { key: 'invalid_confirmation_code' },
  257. })
  258. }
  259. try {
  260. await UserAuditLogHandler.promises.addEntry(
  261. userId,
  262. 'add-email-via-code',
  263. userId,
  264. req.ip,
  265. {
  266. newSecondaryEmail: req.session.pendingSecondaryEmail.email,
  267. }
  268. )
  269. await UserUpdater.promises.addEmailAddress(
  270. userId,
  271. req.session.pendingSecondaryEmail.email,
  272. {},
  273. {
  274. initiatorId: user._id,
  275. ipAddress: req.ip,
  276. }
  277. )
  278. await UserUpdater.promises.confirmEmail(
  279. userId,
  280. req.session.pendingSecondaryEmail.email,
  281. {}
  282. )
  283. delete req.session.pendingSecondaryEmail
  284. AnalyticsManager.recordEventForUserInBackground(
  285. user._id,
  286. 'email-verified',
  287. {
  288. provider: 'email',
  289. verification_type: 'token',
  290. isPrimary: false,
  291. }
  292. )
  293. const redirectUrl =
  294. AuthenticationController.getRedirectFromSession(req) || '/project'
  295. return res.json({
  296. redir: redirectUrl,
  297. })
  298. } catch (error) {
  299. if (error.name === 'EmailExistsError') {
  300. return res.status(409).json({
  301. message: {
  302. type: 'error',
  303. text: req.i18n.translate('email_already_registered'),
  304. },
  305. })
  306. }
  307. logger.err({ error }, 'failed to check confirmation code')
  308. return res.status(500).json({
  309. message: {
  310. key: 'error_performing_request',
  311. },
  312. })
  313. }
  314. }
  315. async function resendSecondaryEmailConfirmationCode(req, res) {
  316. if (!req.session.pendingSecondaryEmail) {
  317. logger.err(
  318. {},
  319. 'error resending confirmation code. missing pendingSecondaryEmail'
  320. )
  321. return res.status(500).json({
  322. message: {
  323. key: 'error_performing_request',
  324. },
  325. })
  326. }
  327. const email = req.session.pendingSecondaryEmail.email
  328. try {
  329. await resendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
  330. method: 'email',
  331. })
  332. } catch (err) {
  333. if (err?.remainingPoints === 0) {
  334. return res.status(429).json({})
  335. } else {
  336. throw err
  337. }
  338. }
  339. try {
  340. const userId = SessionManager.getLoggedInUserId(req.session)
  341. await UserAuditLogHandler.promises.addEntry(
  342. userId,
  343. 'resend-add-email-code',
  344. userId,
  345. req.ip,
  346. {
  347. newSecondaryEmail: email,
  348. }
  349. )
  350. const { confirmCode, confirmCodeExpiresTimestamp } =
  351. await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
  352. email,
  353. true
  354. )
  355. req.session.pendingSecondaryEmail.confirmCode = confirmCode
  356. req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp =
  357. confirmCodeExpiresTimestamp
  358. return res.status(200).json({
  359. message: { key: 'we_sent_new_code' },
  360. })
  361. } catch (err) {
  362. logger.err({ err, email }, 'failed to send confirmation code')
  363. return res.status(500).json({
  364. key: 'error_performing_request',
  365. })
  366. }
  367. }
  368. async function confirmSecondaryEmailPage(req, res) {
  369. const userId = SessionManager.getLoggedInUserId(req.session)
  370. if (!req.session.pendingSecondaryEmail) {
  371. const redirectURL =
  372. AuthenticationController.getRedirectFromSession(req) || '/project'
  373. return res.redirect(redirectURL)
  374. }
  375. AnalyticsManager.recordEventForUserInBackground(
  376. userId,
  377. 'confirm-secondary-email-page-displayed'
  378. )
  379. res.render('user/confirmSecondaryEmail', {
  380. email: req.session.pendingSecondaryEmail.email,
  381. })
  382. }
  383. async function addSecondaryEmailPage(req, res) {
  384. const userId = SessionManager.getLoggedInUserId(req.session)
  385. const confirmedEmails =
  386. await UserGetter.promises.getUserConfirmedEmails(userId)
  387. if (confirmedEmails.length >= 2) {
  388. const redirectURL =
  389. AuthenticationController.getRedirectFromSession(req) || '/project'
  390. return res.redirect(redirectURL)
  391. }
  392. AnalyticsManager.recordEventForUserInBackground(
  393. userId,
  394. 'add-secondary-email-page-displayed'
  395. )
  396. res.render('user/addSecondaryEmail')
  397. }
  398. async function primaryEmailCheckPage(req, res) {
  399. const userId = SessionManager.getLoggedInUserId(req.session)
  400. const user = await UserGetter.promises.getUser(userId, {
  401. lastPrimaryEmailCheck: 1,
  402. signUpDate: 1,
  403. email: 1,
  404. emails: 1,
  405. })
  406. if (!UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)) {
  407. return res.redirect('/project')
  408. }
  409. AnalyticsManager.recordEventForUserInBackground(
  410. userId,
  411. 'primary-email-check-page-displayed'
  412. )
  413. res.render('user/primaryEmailCheck')
  414. }
  415. async function primaryEmailCheck(req, res) {
  416. const userId = SessionManager.getLoggedInUserId(req.session)
  417. await UserUpdater.promises.updateUser(userId, {
  418. $set: { lastPrimaryEmailCheck: new Date() },
  419. })
  420. AnalyticsManager.recordEventForUserInBackground(
  421. userId,
  422. 'primary-email-check-done'
  423. )
  424. // We want to redirect to prompt a user to add a secondary email if their primary
  425. // is an institutional email and they dont' already have a secondary.
  426. if (Features.hasFeature('saas') && req.capabilitySet.has('add-affiliation')) {
  427. const confirmedEmails =
  428. await UserGetter.promises.getUserConfirmedEmails(userId)
  429. if (confirmedEmails.length < 2) {
  430. const { email: primaryEmail } = SessionManager.getSessionUser(req.session)
  431. const primaryEmailDomain = EmailHelper.getDomain(primaryEmail)
  432. const institution = (
  433. await Modules.promises.hooks.fire(
  434. 'getInstitutionViaDomain',
  435. primaryEmailDomain
  436. )
  437. )?.[0]
  438. if (institution) {
  439. return AsyncFormHelper.redirect(req, res, '/user/emails/add-secondary')
  440. }
  441. }
  442. }
  443. AsyncFormHelper.redirect(req, res, '/project')
  444. }
  445. async function showConfirm(req, res, next) {
  446. res.render('user/confirm_email', {
  447. token: req.query.token,
  448. title: 'confirm_email',
  449. })
  450. }
  451. const UserEmailsController = {
  452. list(req, res, next) {
  453. const userId = SessionManager.getLoggedInUserId(req.session)
  454. UserGetter.getUserFullEmails(userId, function (error, fullEmails) {
  455. if (error) {
  456. return next(error)
  457. }
  458. res.json(fullEmails)
  459. })
  460. },
  461. add: expressify(add),
  462. addWithConfirmationCode: expressify(addWithConfirmationCode),
  463. checkSecondaryEmailConfirmationCode: expressify(
  464. checkSecondaryEmailConfirmationCode
  465. ),
  466. resendSecondaryEmailConfirmationCode: expressify(
  467. resendSecondaryEmailConfirmationCode
  468. ),
  469. remove(req, res, next) {
  470. const userId = SessionManager.getLoggedInUserId(req.session)
  471. const email = EmailHelper.parseEmail(req.body.email)
  472. if (!email) {
  473. return res.sendStatus(422)
  474. }
  475. const auditLog = {
  476. initiatorId: userId,
  477. ipAddress: req.ip,
  478. }
  479. UserUpdater.removeEmailAddress(userId, email, auditLog, function (error) {
  480. if (error) {
  481. return next(error)
  482. }
  483. res.sendStatus(200)
  484. })
  485. },
  486. setDefault(req, res, next) {
  487. const userId = SessionManager.getLoggedInUserId(req.session)
  488. const email = EmailHelper.parseEmail(req.body.email)
  489. if (!email) {
  490. return res.sendStatus(422)
  491. }
  492. const auditLog = {
  493. initiatorId: userId,
  494. ipAddress: req.ip,
  495. }
  496. UserUpdater.setDefaultEmailAddress(
  497. userId,
  498. email,
  499. false,
  500. auditLog,
  501. true,
  502. err => {
  503. if (err) {
  504. return UserEmailsController._handleEmailError(err, req, res, next)
  505. }
  506. SessionManager.setInSessionUser(req.session, { email })
  507. const user = SessionManager.getSessionUser(req.session)
  508. UserSessionsManager.removeSessionsFromRedis(
  509. user,
  510. req.sessionID, // remove all sessions except the current session
  511. err => {
  512. if (err)
  513. logger.warn(
  514. { err },
  515. 'failed revoking secondary sessions after changing default email'
  516. )
  517. }
  518. )
  519. res.sendStatus(200)
  520. }
  521. )
  522. },
  523. endorse(req, res, next) {
  524. const userId = SessionManager.getLoggedInUserId(req.session)
  525. const email = EmailHelper.parseEmail(req.body.email)
  526. if (!email) {
  527. return res.sendStatus(422)
  528. }
  529. endorseAffiliation(
  530. userId,
  531. email,
  532. req.body.role,
  533. req.body.department,
  534. function (error) {
  535. if (error) {
  536. return next(error)
  537. }
  538. res.sendStatus(204)
  539. }
  540. )
  541. },
  542. resendConfirmation,
  543. sendReconfirmation,
  544. addSecondaryEmailPage: expressify(addSecondaryEmailPage),
  545. confirmSecondaryEmailPage: expressify(confirmSecondaryEmailPage),
  546. primaryEmailCheckPage: expressify(primaryEmailCheckPage),
  547. primaryEmailCheck: expressify(primaryEmailCheck),
  548. showConfirm: expressify(showConfirm),
  549. confirm(req, res, next) {
  550. const { token } = req.body
  551. if (!token) {
  552. return res.status(422).json({
  553. message: req.i18n.translate('confirmation_link_broken'),
  554. })
  555. }
  556. UserEmailsConfirmationHandler.confirmEmailFromToken(
  557. req,
  558. token,
  559. function (error, userData) {
  560. if (error) {
  561. if (error instanceof Errors.ForbiddenError) {
  562. res.status(403).json({
  563. message: {
  564. key: 'confirm-email-wrong-user',
  565. text: `We can’t confirm this email. You must be logged in with the Overleaf account that requested the new secondary email.`,
  566. },
  567. })
  568. } else if (error instanceof Errors.NotFoundError) {
  569. res.status(404).json({
  570. message: req.i18n.translate('confirmation_token_invalid'),
  571. })
  572. } else {
  573. next(error)
  574. }
  575. } else {
  576. const { userId, email } = userData
  577. const tokenPrefix = token.substring(0, AUDIT_LOG_TOKEN_PREFIX_LENGTH)
  578. UserAuditLogHandler.addEntry(
  579. userId,
  580. 'confirm-email',
  581. userId,
  582. req.ip,
  583. { token: tokenPrefix, email },
  584. auditLogError => {
  585. if (auditLogError) {
  586. logger.error(
  587. { error: auditLogError, userId, token: tokenPrefix },
  588. 'failed to add audit log entry'
  589. )
  590. }
  591. UserGetter.getUser(
  592. userData.userId,
  593. { email: 1 },
  594. function (error, user) {
  595. if (error) {
  596. logger.error(
  597. { error, userId: userData.userId },
  598. 'failed to get user'
  599. )
  600. }
  601. const isPrimary = user?.email === userData.email
  602. AnalyticsManager.recordEventForUserInBackground(
  603. userData.userId,
  604. 'email-verified',
  605. {
  606. provider: 'email',
  607. verification_type: 'link',
  608. isPrimary,
  609. }
  610. )
  611. res.sendStatus(200)
  612. }
  613. )
  614. }
  615. )
  616. }
  617. }
  618. )
  619. },
  620. _handleEmailError(error, req, res, next) {
  621. if (error instanceof Errors.UnconfirmedEmailError) {
  622. return HttpErrorHandler.conflict(req, res, 'email must be confirmed')
  623. } else if (error instanceof Errors.EmailExistsError) {
  624. const message = req.i18n.translate('email_already_registered')
  625. return HttpErrorHandler.conflict(req, res, message)
  626. } else if (error.message === '422: Email does not belong to university') {
  627. const message = req.i18n.translate('email_does_not_belong_to_university')
  628. return HttpErrorHandler.conflict(req, res, message)
  629. }
  630. next(error)
  631. },
  632. }
  633. module.exports = UserEmailsController