UserEmailsController.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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.recordEventForUser(user._id, 'email-verified', {
  285. provider: 'email',
  286. verification_type: 'token',
  287. isPrimary: false,
  288. })
  289. const redirectUrl =
  290. AuthenticationController.getRedirectFromSession(req) || '/project'
  291. return res.json({
  292. redir: redirectUrl,
  293. })
  294. } catch (error) {
  295. if (error.name === 'EmailExistsError') {
  296. return res.status(409).json({
  297. message: {
  298. type: 'error',
  299. text: req.i18n.translate('email_already_registered'),
  300. },
  301. })
  302. }
  303. logger.err({ error }, 'failed to check confirmation code')
  304. return res.status(500).json({
  305. message: {
  306. key: 'error_performing_request',
  307. },
  308. })
  309. }
  310. }
  311. async function resendSecondaryEmailConfirmationCode(req, res) {
  312. if (!req.session.pendingSecondaryEmail) {
  313. logger.err(
  314. {},
  315. 'error resending confirmation code. missing pendingSecondaryEmail'
  316. )
  317. return res.status(500).json({
  318. message: {
  319. key: 'error_performing_request',
  320. },
  321. })
  322. }
  323. const email = req.session.pendingSecondaryEmail.email
  324. try {
  325. await resendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
  326. method: 'email',
  327. })
  328. } catch (err) {
  329. if (err?.remainingPoints === 0) {
  330. return res.status(429).json({})
  331. } else {
  332. throw err
  333. }
  334. }
  335. try {
  336. const userId = SessionManager.getLoggedInUserId(req.session)
  337. await UserAuditLogHandler.promises.addEntry(
  338. userId,
  339. 'resend-add-email-code',
  340. userId,
  341. req.ip,
  342. {
  343. newSecondaryEmail: email,
  344. }
  345. )
  346. const { confirmCode, confirmCodeExpiresTimestamp } =
  347. await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
  348. email,
  349. true
  350. )
  351. req.session.pendingSecondaryEmail.confirmCode = confirmCode
  352. req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp =
  353. confirmCodeExpiresTimestamp
  354. return res.status(200).json({
  355. message: { key: 'we_sent_new_code' },
  356. })
  357. } catch (err) {
  358. logger.err({ err, email }, 'failed to send confirmation code')
  359. return res.status(500).json({
  360. key: 'error_performing_request',
  361. })
  362. }
  363. }
  364. async function primaryEmailCheckPage(req, res) {
  365. const userId = SessionManager.getLoggedInUserId(req.session)
  366. const user = await UserGetter.promises.getUser(userId, {
  367. lastPrimaryEmailCheck: 1,
  368. signUpDate: 1,
  369. email: 1,
  370. emails: 1,
  371. })
  372. if (!UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)) {
  373. return res.redirect('/project')
  374. }
  375. AnalyticsManager.recordEventForUser(
  376. userId,
  377. 'primary-email-check-page-displayed'
  378. )
  379. res.render('user/primaryEmailCheck')
  380. }
  381. async function primaryEmailCheck(req, res) {
  382. const userId = SessionManager.getLoggedInUserId(req.session)
  383. await UserUpdater.promises.updateUser(userId, {
  384. $set: { lastPrimaryEmailCheck: new Date() },
  385. })
  386. AnalyticsManager.recordEventForUser(userId, 'primary-email-check-done')
  387. AsyncFormHelper.redirect(req, res, '/project')
  388. }
  389. async function showConfirm(req, res, next) {
  390. res.render('user/confirm_email', {
  391. token: req.query.token,
  392. title: 'confirm_email',
  393. })
  394. }
  395. const UserEmailsController = {
  396. list(req, res, next) {
  397. const userId = SessionManager.getLoggedInUserId(req.session)
  398. UserGetter.getUserFullEmails(userId, function (error, fullEmails) {
  399. if (error) {
  400. return next(error)
  401. }
  402. res.json(fullEmails)
  403. })
  404. },
  405. add: expressify(add),
  406. addWithConfirmationCode: expressify(addWithConfirmationCode),
  407. checkSecondaryEmailConfirmationCode: expressify(
  408. checkSecondaryEmailConfirmationCode
  409. ),
  410. resendSecondaryEmailConfirmationCode: expressify(
  411. resendSecondaryEmailConfirmationCode
  412. ),
  413. remove(req, res, next) {
  414. const userId = SessionManager.getLoggedInUserId(req.session)
  415. const email = EmailHelper.parseEmail(req.body.email)
  416. if (!email) {
  417. return res.sendStatus(422)
  418. }
  419. const auditLog = {
  420. initiatorId: userId,
  421. ipAddress: req.ip,
  422. }
  423. UserUpdater.removeEmailAddress(userId, email, auditLog, function (error) {
  424. if (error) {
  425. return next(error)
  426. }
  427. res.sendStatus(200)
  428. })
  429. },
  430. setDefault(req, res, next) {
  431. const userId = SessionManager.getLoggedInUserId(req.session)
  432. const email = EmailHelper.parseEmail(req.body.email)
  433. if (!email) {
  434. return res.sendStatus(422)
  435. }
  436. const auditLog = {
  437. initiatorId: userId,
  438. ipAddress: req.ip,
  439. }
  440. UserUpdater.setDefaultEmailAddress(
  441. userId,
  442. email,
  443. false,
  444. auditLog,
  445. true,
  446. err => {
  447. if (err) {
  448. return UserEmailsController._handleEmailError(err, req, res, next)
  449. }
  450. SessionManager.setInSessionUser(req.session, { email })
  451. const user = SessionManager.getSessionUser(req.session)
  452. UserSessionsManager.revokeAllUserSessions(
  453. user,
  454. [req.sessionID],
  455. err => {
  456. if (err)
  457. logger.warn(
  458. { err },
  459. 'failed revoking secondary sessions after changing default email'
  460. )
  461. }
  462. )
  463. res.sendStatus(200)
  464. }
  465. )
  466. },
  467. endorse(req, res, next) {
  468. const userId = SessionManager.getLoggedInUserId(req.session)
  469. const email = EmailHelper.parseEmail(req.body.email)
  470. if (!email) {
  471. return res.sendStatus(422)
  472. }
  473. endorseAffiliation(
  474. userId,
  475. email,
  476. req.body.role,
  477. req.body.department,
  478. function (error) {
  479. if (error) {
  480. return next(error)
  481. }
  482. res.sendStatus(204)
  483. }
  484. )
  485. },
  486. resendConfirmation,
  487. sendReconfirmation,
  488. primaryEmailCheckPage: expressify(primaryEmailCheckPage),
  489. primaryEmailCheck: expressify(primaryEmailCheck),
  490. showConfirm: expressify(showConfirm),
  491. confirm(req, res, next) {
  492. const { token } = req.body
  493. if (!token) {
  494. return res.status(422).json({
  495. message: req.i18n.translate('confirmation_link_broken'),
  496. })
  497. }
  498. UserEmailsConfirmationHandler.confirmEmailFromToken(
  499. req,
  500. token,
  501. function (error, userData) {
  502. if (error) {
  503. if (error instanceof Errors.ForbiddenError) {
  504. res.status(403).json({
  505. message: {
  506. key: 'confirm-email-wrong-user',
  507. text: `We can’t confirm this email. You must be logged in with the Overleaf account that requested the new secondary email.`,
  508. },
  509. })
  510. } else if (error instanceof Errors.NotFoundError) {
  511. res.status(404).json({
  512. message: req.i18n.translate('confirmation_token_invalid'),
  513. })
  514. } else {
  515. next(error)
  516. }
  517. } else {
  518. const { userId, email } = userData
  519. const tokenPrefix = token.substring(0, AUDIT_LOG_TOKEN_PREFIX_LENGTH)
  520. UserAuditLogHandler.addEntry(
  521. userId,
  522. 'confirm-email',
  523. userId,
  524. req.ip,
  525. { token: tokenPrefix, email },
  526. auditLogError => {
  527. if (auditLogError) {
  528. logger.error(
  529. { error: auditLogError, userId, token: tokenPrefix },
  530. 'failed to add audit log entry'
  531. )
  532. }
  533. UserGetter.getUser(
  534. userData.userId,
  535. { email: 1 },
  536. function (error, user) {
  537. if (error) {
  538. logger.error(
  539. { error, userId: userData.userId },
  540. 'failed to get user'
  541. )
  542. }
  543. const isPrimary = user?.email === userData.email
  544. AnalyticsManager.recordEventForUser(
  545. userData.userId,
  546. 'email-verified',
  547. {
  548. provider: 'email',
  549. verification_type: 'link',
  550. isPrimary,
  551. }
  552. )
  553. res.sendStatus(200)
  554. }
  555. )
  556. }
  557. )
  558. }
  559. }
  560. )
  561. },
  562. _handleEmailError(error, req, res, next) {
  563. if (error instanceof Errors.UnconfirmedEmailError) {
  564. return HttpErrorHandler.conflict(req, res, 'email must be confirmed')
  565. } else if (error instanceof Errors.EmailExistsError) {
  566. const message = req.i18n.translate('email_already_registered')
  567. return HttpErrorHandler.conflict(req, res, message)
  568. } else if (error.message === '422: Email does not belong to university') {
  569. const message = req.i18n.translate('email_does_not_belong_to_university')
  570. return HttpErrorHandler.conflict(req, res, message)
  571. }
  572. next(error)
  573. },
  574. }
  575. module.exports = UserEmailsController