UserEmailsConfirmationHandler.mjs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import EmailHelper from '../Helpers/EmailHelper.mjs'
  2. import EmailHandler from '../Email/EmailHandler.mjs'
  3. import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs'
  4. import settings from '@overleaf/settings'
  5. import Errors from '../Errors/Errors.js'
  6. import UserUpdater from './UserUpdater.mjs'
  7. import UserGetter from './UserGetter.mjs'
  8. import { callbackify } from 'node:util'
  9. import crypto from 'node:crypto'
  10. import SessionManager from '../Authentication/SessionManager.mjs'
  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. async function sendConfirmationEmail(
  16. userId,
  17. email,
  18. emailTemplate = 'confirmEmail'
  19. ) {
  20. email = EmailHelper.parseEmail(email)
  21. if (!email) {
  22. throw new Error('invalid email')
  23. }
  24. const data = { user_id: userId, email }
  25. const token = await OneTimeTokenHandler.promises.getNewToken(
  26. TOKEN_USE,
  27. data,
  28. { expiresIn: TOKEN_EXPIRY_IN_S }
  29. )
  30. const emailOptions = {
  31. to: email,
  32. confirmEmailUrl: `${settings.siteUrl}/user/emails/confirm?token=${token}`,
  33. sendingUser_id: userId,
  34. }
  35. await EmailHandler.promises.sendEmail(emailTemplate, emailOptions)
  36. }
  37. async function sendConfirmationCode(email, welcomeUser) {
  38. if (!EmailHelper.parseEmail(email)) {
  39. throw new Error('invalid email')
  40. }
  41. const confirmCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, '0')
  42. const confirmCodeExpiresTimestamp =
  43. Date.now() + CONFIRMATION_CODE_EXPIRY_IN_S * 1000
  44. await EmailHandler.promises.sendEmail('confirmCode', {
  45. to: email,
  46. confirmCode,
  47. welcomeUser,
  48. category: ['ConfirmEmail'],
  49. })
  50. return {
  51. confirmCode,
  52. confirmCodeExpiresTimestamp,
  53. }
  54. }
  55. async function confirmEmailFromToken(req, token) {
  56. const { data } = await OneTimeTokenHandler.promises.peekValueFromToken(
  57. TOKEN_USE,
  58. token
  59. )
  60. if (!data) {
  61. throw new Errors.NotFoundError('no token found')
  62. }
  63. const loggedInUserId = SessionManager.getLoggedInUserId(req.session)
  64. // user_id may be stored as an ObjectId or string
  65. const userId = data.user_id?.toString()
  66. const email = data.email
  67. if (!userId || email !== EmailHelper.parseEmail(email)) {
  68. throw new Errors.NotFoundError('invalid data')
  69. }
  70. if (loggedInUserId !== userId) {
  71. throw new Errors.ForbiddenError('logged in user does not match token user')
  72. }
  73. const user = await UserGetter.promises.getUser(userId, { emails: 1 })
  74. if (!user) {
  75. throw new Errors.NotFoundError('user not found')
  76. }
  77. const emailExists = user.emails.some(emailData => emailData.email === email)
  78. if (!emailExists) {
  79. throw new Errors.NotFoundError('email missing for user')
  80. }
  81. await OneTimeTokenHandler.promises.expireToken(TOKEN_USE, token)
  82. await UserUpdater.promises.confirmEmail(userId, email)
  83. return { userId, email }
  84. }
  85. const UserEmailsConfirmationHandler = {
  86. confirmEmailFromToken: callbackify(confirmEmailFromToken),
  87. sendConfirmationEmail: callbackify(sendConfirmationEmail),
  88. }
  89. UserEmailsConfirmationHandler.promises = {
  90. sendConfirmationEmail,
  91. confirmEmailFromToken,
  92. sendConfirmationCode,
  93. }
  94. export default UserEmailsConfirmationHandler