UserEmailsController.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  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 tsscmp = require('tsscmp')
  21. const AUDIT_LOG_TOKEN_PREFIX_LENGTH = 10
  22. const sendSecondaryConfirmCodeRateLimiter = new RateLimiter(
  23. 'send-secondary-confirmation-code',
  24. {
  25. points: 1,
  26. duration: 60,
  27. }
  28. )
  29. const checkSecondaryConfirmCodeRateLimiter = new RateLimiter(
  30. 'check-secondary-confirmation-code-per-email',
  31. {
  32. points: 10,
  33. duration: 60,
  34. }
  35. )
  36. const resendSecondaryConfirmCodeRateLimiter = new RateLimiter(
  37. 'resend-secondary-confirmation-code',
  38. {
  39. points: 1,
  40. duration: 60,
  41. }
  42. )
  43. async function _sendSecurityAlertEmail(user, email) {
  44. const emailOptions = {
  45. to: user.email,
  46. actionDescribed: `a secondary email address has been added to your account ${user.email}`,
  47. message: [
  48. `<span style="display:inline-block;padding: 0 20px;width:100%;">Added: <br/><b>${email}</b></span>`,
  49. ],
  50. action: 'secondary email address added',
  51. }
  52. await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
  53. }
  54. /**
  55. * This method is for adding a secondary email to be confirmed via an emailed link.
  56. * For code confirmation, see the `addWithConfirmationCode` method in this file.
  57. */
  58. async function add(req, res, next) {
  59. const userId = SessionManager.getLoggedInUserId(req.session)
  60. const email = EmailHelper.parseEmail(req.body.email)
  61. if (!email) {
  62. return res.sendStatus(422)
  63. }
  64. const user = await UserGetter.promises.getUser(userId, {
  65. email: 1,
  66. 'emails.email': 1,
  67. })
  68. if (user.emails.length >= Settings.emailAddressLimit) {
  69. return res.status(422).json({ message: 'secondary email limit exceeded' })
  70. }
  71. const affiliationOptions = {
  72. university: req.body.university,
  73. role: req.body.role,
  74. department: req.body.department,
  75. }
  76. try {
  77. await UserUpdater.promises.addEmailAddress(
  78. userId,
  79. email,
  80. affiliationOptions,
  81. {
  82. initiatorId: user._id,
  83. ipAddress: req.ip,
  84. }
  85. )
  86. } catch (error) {
  87. return UserEmailsController._handleEmailError(error, req, res, next)
  88. }
  89. await _sendSecurityAlertEmail(user, email)
  90. await UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
  91. userId,
  92. email
  93. )
  94. res.sendStatus(204)
  95. }
  96. function resendConfirmation(req, res, next) {
  97. const userId = SessionManager.getLoggedInUserId(req.session)
  98. const email = EmailHelper.parseEmail(req.body.email)
  99. if (!email) {
  100. return res.sendStatus(422)
  101. }
  102. UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (error, user) {
  103. if (error) {
  104. return next(error)
  105. }
  106. if (!user || user._id.toString() !== userId) {
  107. return res.sendStatus(422)
  108. }
  109. UserEmailsConfirmationHandler.sendConfirmationEmail(
  110. userId,
  111. email,
  112. function (error) {
  113. if (error) {
  114. return next(error)
  115. }
  116. res.sendStatus(200)
  117. }
  118. )
  119. })
  120. }
  121. function sendReconfirmation(req, res, next) {
  122. const userId = SessionManager.getLoggedInUserId(req.session)
  123. const email = EmailHelper.parseEmail(req.body.email)
  124. if (!email) {
  125. return res.sendStatus(400)
  126. }
  127. UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (error, user) {
  128. if (error) {
  129. return next(error)
  130. }
  131. if (!user || user._id.toString() !== userId) {
  132. return res.sendStatus(422)
  133. }
  134. UserEmailsConfirmationHandler.sendReconfirmationEmail(
  135. userId,
  136. email,
  137. function (error) {
  138. if (error) {
  139. return next(error)
  140. }
  141. res.sendStatus(204)
  142. }
  143. )
  144. })
  145. }
  146. /**
  147. * This method is for adding a secondary email to be confirmed via a code.
  148. * For email link confirmation see the `add` method in this file.
  149. */
  150. async function addWithConfirmationCode(req, res) {
  151. delete req.session.pendingSecondaryEmail
  152. const userId = SessionManager.getLoggedInUserId(req.session)
  153. const email = EmailHelper.parseEmail(req.body.email)
  154. if (!email) {
  155. return res.sendStatus(422)
  156. }
  157. const user = await UserGetter.promises.getUser(userId, {
  158. email: 1,
  159. 'emails.email': 1,
  160. })
  161. if (user.emails.length >= Settings.emailAddressLimit) {
  162. return res.status(422).json({ message: 'secondary email limit exceeded' })
  163. }
  164. try {
  165. await UserGetter.promises.ensureUniqueEmailAddress(email)
  166. await sendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
  167. method: 'email',
  168. })
  169. await UserAuditLogHandler.promises.addEntry(
  170. userId,
  171. 'request-add-email-code',
  172. userId,
  173. req.ip,
  174. {
  175. newSecondaryEmail: email,
  176. }
  177. )
  178. const { confirmCode, confirmCodeExpiresTimestamp } =
  179. await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
  180. email,
  181. true
  182. )
  183. req.session.pendingSecondaryEmail = {
  184. email,
  185. confirmCode,
  186. confirmCodeExpiresTimestamp,
  187. }
  188. return res.json({
  189. redir: '/user/emails/confirm-secondary',
  190. })
  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 primaryEmailCheckPage(req, res) {
  369. const userId = SessionManager.getLoggedInUserId(req.session)
  370. const user = await UserGetter.promises.getUser(userId, {
  371. lastPrimaryEmailCheck: 1,
  372. signUpDate: 1,
  373. email: 1,
  374. emails: 1,
  375. })
  376. if (!UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)) {
  377. return res.redirect('/project')
  378. }
  379. AnalyticsManager.recordEventForUserInBackground(
  380. userId,
  381. 'primary-email-check-page-displayed'
  382. )
  383. res.render('user/primaryEmailCheck')
  384. }
  385. async function primaryEmailCheck(req, res) {
  386. const userId = SessionManager.getLoggedInUserId(req.session)
  387. await UserUpdater.promises.updateUser(userId, {
  388. $set: { lastPrimaryEmailCheck: new Date() },
  389. })
  390. AnalyticsManager.recordEventForUserInBackground(
  391. userId,
  392. 'primary-email-check-done'
  393. )
  394. AsyncFormHelper.redirect(req, res, '/project')
  395. }
  396. async function showConfirm(req, res, next) {
  397. res.render('user/confirm_email', {
  398. token: req.query.token,
  399. title: 'confirm_email',
  400. })
  401. }
  402. const UserEmailsController = {
  403. list(req, res, next) {
  404. const userId = SessionManager.getLoggedInUserId(req.session)
  405. UserGetter.getUserFullEmails(userId, function (error, fullEmails) {
  406. if (error) {
  407. return next(error)
  408. }
  409. res.json(fullEmails)
  410. })
  411. },
  412. add: expressify(add),
  413. addWithConfirmationCode: expressify(addWithConfirmationCode),
  414. checkSecondaryEmailConfirmationCode: expressify(
  415. checkSecondaryEmailConfirmationCode
  416. ),
  417. resendSecondaryEmailConfirmationCode: expressify(
  418. resendSecondaryEmailConfirmationCode
  419. ),
  420. remove(req, res, next) {
  421. const userId = SessionManager.getLoggedInUserId(req.session)
  422. const email = EmailHelper.parseEmail(req.body.email)
  423. if (!email) {
  424. return res.sendStatus(422)
  425. }
  426. const auditLog = {
  427. initiatorId: userId,
  428. ipAddress: req.ip,
  429. }
  430. UserUpdater.removeEmailAddress(userId, email, auditLog, function (error) {
  431. if (error) {
  432. return next(error)
  433. }
  434. res.sendStatus(200)
  435. })
  436. },
  437. setDefault(req, res, next) {
  438. const userId = SessionManager.getLoggedInUserId(req.session)
  439. const email = EmailHelper.parseEmail(req.body.email)
  440. if (!email) {
  441. return res.sendStatus(422)
  442. }
  443. const auditLog = {
  444. initiatorId: userId,
  445. ipAddress: req.ip,
  446. }
  447. UserUpdater.setDefaultEmailAddress(
  448. userId,
  449. email,
  450. false,
  451. auditLog,
  452. true,
  453. err => {
  454. if (err) {
  455. return UserEmailsController._handleEmailError(err, req, res, next)
  456. }
  457. SessionManager.setInSessionUser(req.session, { email })
  458. const user = SessionManager.getSessionUser(req.session)
  459. UserSessionsManager.removeSessionsFromRedis(
  460. user,
  461. req.sessionID, // remove all sessions except the current session
  462. err => {
  463. if (err)
  464. logger.warn(
  465. { err },
  466. 'failed revoking secondary sessions after changing default email'
  467. )
  468. }
  469. )
  470. res.sendStatus(200)
  471. }
  472. )
  473. },
  474. endorse(req, res, next) {
  475. const userId = SessionManager.getLoggedInUserId(req.session)
  476. const email = EmailHelper.parseEmail(req.body.email)
  477. if (!email) {
  478. return res.sendStatus(422)
  479. }
  480. endorseAffiliation(
  481. userId,
  482. email,
  483. req.body.role,
  484. req.body.department,
  485. function (error) {
  486. if (error) {
  487. return next(error)
  488. }
  489. res.sendStatus(204)
  490. }
  491. )
  492. },
  493. resendConfirmation,
  494. sendReconfirmation,
  495. primaryEmailCheckPage: expressify(primaryEmailCheckPage),
  496. primaryEmailCheck: expressify(primaryEmailCheck),
  497. showConfirm: expressify(showConfirm),
  498. confirm(req, res, next) {
  499. const { token } = req.body
  500. if (!token) {
  501. return res.status(422).json({
  502. message: req.i18n.translate('confirmation_link_broken'),
  503. })
  504. }
  505. UserEmailsConfirmationHandler.confirmEmailFromToken(
  506. req,
  507. token,
  508. function (error, userData) {
  509. if (error) {
  510. if (error instanceof Errors.ForbiddenError) {
  511. res.status(403).json({
  512. message: {
  513. key: 'confirm-email-wrong-user',
  514. text: `We can’t confirm this email. You must be logged in with the Overleaf account that requested the new secondary email.`,
  515. },
  516. })
  517. } else if (error instanceof Errors.NotFoundError) {
  518. res.status(404).json({
  519. message: req.i18n.translate('confirmation_token_invalid'),
  520. })
  521. } else {
  522. next(error)
  523. }
  524. } else {
  525. const { userId, email } = userData
  526. const tokenPrefix = token.substring(0, AUDIT_LOG_TOKEN_PREFIX_LENGTH)
  527. UserAuditLogHandler.addEntry(
  528. userId,
  529. 'confirm-email',
  530. userId,
  531. req.ip,
  532. { token: tokenPrefix, email },
  533. auditLogError => {
  534. if (auditLogError) {
  535. logger.error(
  536. { error: auditLogError, userId, token: tokenPrefix },
  537. 'failed to add audit log entry'
  538. )
  539. }
  540. UserGetter.getUser(
  541. userData.userId,
  542. { email: 1 },
  543. function (error, user) {
  544. if (error) {
  545. logger.error(
  546. { error, userId: userData.userId },
  547. 'failed to get user'
  548. )
  549. }
  550. const isPrimary = user?.email === userData.email
  551. AnalyticsManager.recordEventForUserInBackground(
  552. userData.userId,
  553. 'email-verified',
  554. {
  555. provider: 'email',
  556. verification_type: 'link',
  557. isPrimary,
  558. }
  559. )
  560. res.sendStatus(200)
  561. }
  562. )
  563. }
  564. )
  565. }
  566. }
  567. )
  568. },
  569. _handleEmailError(error, req, res, next) {
  570. if (error instanceof Errors.UnconfirmedEmailError) {
  571. return HttpErrorHandler.conflict(req, res, 'email must be confirmed')
  572. } else if (error instanceof Errors.EmailExistsError) {
  573. const message = req.i18n.translate('email_already_registered')
  574. return HttpErrorHandler.conflict(req, res, message)
  575. } else if (error.message === '422: Email does not belong to university') {
  576. const message = req.i18n.translate('email_does_not_belong_to_university')
  577. return HttpErrorHandler.conflict(req, res, message)
  578. }
  579. next(error)
  580. },
  581. }
  582. module.exports = UserEmailsController