UserEmailsConfirmationHandler.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. const EmailHelper = require('../Helpers/EmailHelper')
  2. const EmailHandler = require('../Email/EmailHandler')
  3. const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler')
  4. const settings = require('@overleaf/settings')
  5. const Errors = require('../Errors/Errors')
  6. const UserUpdater = require('./UserUpdater')
  7. const UserGetter = require('./UserGetter')
  8. const { callbackify, promisify } = require('util')
  9. const crypto = require('crypto')
  10. const SessionManager = require('../Authentication/SessionManager')
  11. // Reject email confirmation tokens after 90 days
  12. const TOKEN_EXPIRY_IN_S = 90 * 24 * 60 * 60
  13. const TOKEN_USE = 'email_confirmation'
  14. const CONFIRMATION_CODE_EXPIRY_IN_S = 10 * 60
  15. function sendConfirmationEmail(userId, email, emailTemplate, callback) {
  16. if (arguments.length === 3) {
  17. callback = emailTemplate
  18. emailTemplate = 'confirmEmail'
  19. }
  20. email = EmailHelper.parseEmail(email)
  21. if (!email) {
  22. return callback(new Error('invalid email'))
  23. }
  24. const data = { user_id: userId, email }
  25. OneTimeTokenHandler.getNewToken(
  26. TOKEN_USE,
  27. data,
  28. { expiresIn: TOKEN_EXPIRY_IN_S },
  29. function (err, token) {
  30. if (err) {
  31. return callback(err)
  32. }
  33. const emailOptions = {
  34. to: email,
  35. confirmEmailUrl: `${settings.siteUrl}/user/emails/confirm?token=${token}`,
  36. sendingUser_id: userId,
  37. }
  38. EmailHandler.sendEmail(emailTemplate, emailOptions, callback)
  39. }
  40. )
  41. }
  42. async function sendConfirmationCode(email, isSecondary) {
  43. if (!EmailHelper.parseEmail(email)) {
  44. throw new Error('invalid email')
  45. }
  46. const confirmCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, '0')
  47. const confirmCodeExpiresTimestamp =
  48. Date.now() + CONFIRMATION_CODE_EXPIRY_IN_S * 1000
  49. await EmailHandler.promises.sendEmail('confirmCode', {
  50. to: email,
  51. confirmCode,
  52. isSecondary,
  53. category: ['ConfirmEmail'],
  54. })
  55. return {
  56. confirmCode,
  57. confirmCodeExpiresTimestamp,
  58. }
  59. }
  60. async function sendReconfirmationEmail(userId, email) {
  61. email = EmailHelper.parseEmail(email)
  62. if (!email) {
  63. throw new Error('invalid email')
  64. }
  65. const data = { user_id: userId, email }
  66. const token = await OneTimeTokenHandler.promises.getNewToken(
  67. TOKEN_USE,
  68. data,
  69. { expiresIn: TOKEN_EXPIRY_IN_S }
  70. )
  71. const emailOptions = {
  72. to: email,
  73. confirmEmailUrl: `${settings.siteUrl}/user/emails/confirm?token=${token}`,
  74. sendingUser_id: userId,
  75. }
  76. await EmailHandler.promises.sendEmail('reconfirmEmail', emailOptions)
  77. }
  78. async function confirmEmailFromToken(req, token) {
  79. const { data } = await OneTimeTokenHandler.promises.peekValueFromToken(
  80. TOKEN_USE,
  81. token
  82. )
  83. if (!data) {
  84. throw new Errors.NotFoundError('no token found')
  85. }
  86. const loggedInUserId = SessionManager.getLoggedInUserId(req.session)
  87. // user_id may be stored as an ObjectId or string
  88. const userId = data.user_id?.toString()
  89. const email = data.email
  90. if (!userId || email !== EmailHelper.parseEmail(email)) {
  91. throw new Errors.NotFoundError('invalid data')
  92. }
  93. if (loggedInUserId !== userId) {
  94. throw new Errors.ForbiddenError('logged in user does not match token user')
  95. }
  96. const user = await UserGetter.promises.getUser(userId, { emails: 1 })
  97. if (!user) {
  98. throw new Errors.NotFoundError('user not found')
  99. }
  100. const emailExists = user.emails.some(emailData => emailData.email === email)
  101. if (!emailExists) {
  102. throw new Errors.NotFoundError('email missing for user')
  103. }
  104. await OneTimeTokenHandler.promises.expireToken(TOKEN_USE, token)
  105. await UserUpdater.promises.confirmEmail(userId, email)
  106. return { userId, email }
  107. }
  108. const UserEmailsConfirmationHandler = {
  109. sendConfirmationEmail,
  110. sendReconfirmationEmail: callbackify(sendReconfirmationEmail),
  111. confirmEmailFromToken: callbackify(confirmEmailFromToken),
  112. }
  113. UserEmailsConfirmationHandler.promises = {
  114. sendConfirmationEmail: promisify(sendConfirmationEmail),
  115. confirmEmailFromToken,
  116. sendConfirmationCode,
  117. }
  118. module.exports = UserEmailsConfirmationHandler