UserEmailsController.mjs 20 KB

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