PasswordResetController.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import PasswordResetHandler from './PasswordResetHandler.mjs'
  2. import AuthenticationController from '../Authentication/AuthenticationController.js'
  3. import AuthenticationManager from '../Authentication/AuthenticationManager.js'
  4. import SessionManager from '../Authentication/SessionManager.js'
  5. import UserGetter from '../User/UserGetter.js'
  6. import UserUpdater from '../User/UserUpdater.js'
  7. import UserSessionsManager from '../User/UserSessionsManager.js'
  8. import OError from '@overleaf/o-error'
  9. import EmailsHelper from '../Helpers/EmailHelper.js'
  10. import { expressify } from '@overleaf/promise-utils'
  11. import { z, validateReq } from '../../infrastructure/Validation.js'
  12. async function setNewUserPassword(req, res, next) {
  13. let user
  14. let { passwordResetToken, password, email } = req.body
  15. if (!passwordResetToken || !password) {
  16. return res.status(400).json({
  17. message: {
  18. key: 'invalid-password',
  19. },
  20. })
  21. }
  22. const err = AuthenticationManager.validatePassword(password, email)
  23. if (err) {
  24. const message = AuthenticationManager.getMessageForInvalidPasswordError(
  25. err,
  26. req
  27. )
  28. return res.status(400).json({ message })
  29. }
  30. passwordResetToken = passwordResetToken.trim()
  31. const initiatorId = SessionManager.getLoggedInUserId(req.session)
  32. // password reset via tokens can be done while logged in, or not
  33. const auditLog = {
  34. initiatorId,
  35. ip: req.ip,
  36. }
  37. try {
  38. const result = await PasswordResetHandler.promises.setNewUserPassword(
  39. passwordResetToken,
  40. password,
  41. auditLog
  42. )
  43. const { found, reset, userId, mustReconfirm } = result
  44. if (!found) {
  45. return res.status(404).json({
  46. message: {
  47. key: 'token-expired',
  48. },
  49. })
  50. }
  51. if (!reset) {
  52. return res.status(500).json({
  53. message: req.i18n.translate('error_performing_request'),
  54. })
  55. }
  56. await UserSessionsManager.promises.removeSessionsFromRedis({ _id: userId })
  57. if (mustReconfirm) {
  58. await UserUpdater.promises.removeReconfirmFlag(userId, auditLog)
  59. }
  60. if (!req.session.doLoginAfterPasswordReset) {
  61. return res.sendStatus(200)
  62. }
  63. user = await UserGetter.promises.getUser(userId)
  64. } catch (error) {
  65. if (error.name === 'NotFoundError') {
  66. return res.status(404).json({
  67. message: {
  68. key: 'token-expired',
  69. },
  70. })
  71. } else if (error.name === 'InvalidPasswordError') {
  72. return res.status(400).json({
  73. message: {
  74. key: 'invalid-password',
  75. },
  76. })
  77. } else if (error.name === 'PasswordMustBeDifferentError') {
  78. return res.status(400).json({
  79. message: {
  80. key: 'password-must-be-different',
  81. },
  82. })
  83. } else if (error.name === 'PasswordReusedError') {
  84. return res.status(400).json({
  85. message: {
  86. key: 'password-must-be-strong',
  87. },
  88. })
  89. } else {
  90. return res.status(500).json({
  91. message: req.i18n.translate('error_performing_request'),
  92. })
  93. }
  94. }
  95. AuthenticationController.setAuditInfo(req, {
  96. method: 'Password reset, set new password',
  97. })
  98. AuthenticationController.finishLogin(user, req, res, next)
  99. }
  100. const requestResetSchema = z.object({
  101. body: z.object({
  102. email: z.string(),
  103. }),
  104. })
  105. async function requestReset(req, res, next) {
  106. const { body } = validateReq(req, requestResetSchema)
  107. const email = EmailsHelper.parseEmail(body.email)
  108. if (!email) {
  109. return res.status(400).json({
  110. message: req.i18n.translate('must_be_email_address'),
  111. })
  112. }
  113. let status
  114. try {
  115. status =
  116. await PasswordResetHandler.promises.generateAndEmailResetToken(email)
  117. } catch (err) {
  118. OError.tag(err, 'failed to generate and email password reset token', {
  119. email,
  120. })
  121. if (
  122. err.message ===
  123. 'user does not have one or more permissions within change-password'
  124. ) {
  125. return res.status(403).json({
  126. message: {
  127. key: 'no-password-allowed-due-to-sso',
  128. },
  129. })
  130. }
  131. throw err
  132. }
  133. if (status === 'primary') {
  134. return res.status(200).json({
  135. message: req.i18n.translate('password_reset_email_sent'),
  136. })
  137. } else if (status === 'secondary') {
  138. return res.status(404).json({
  139. message: req.i18n.translate('secondary_email_password_reset'),
  140. })
  141. } else {
  142. return res.status(404).json({
  143. message: req.i18n.translate('cant_find_email'),
  144. })
  145. }
  146. }
  147. const renderSetPasswordFormSchema = z.object({
  148. query: z.object({
  149. email: z.string(),
  150. passwordResetToken: z.string().optional(),
  151. }),
  152. })
  153. async function renderSetPasswordForm(req, res, next) {
  154. const { query } = validateReq(req, renderSetPasswordFormSchema)
  155. if (query.passwordResetToken != null) {
  156. try {
  157. const result =
  158. await PasswordResetHandler.promises.getUserForPasswordResetToken(
  159. query.passwordResetToken
  160. )
  161. const { user, remainingPeeks } = result || {}
  162. if (!user || remainingPeeks <= 0) {
  163. return res.redirect('/user/password/reset?error=token_expired')
  164. }
  165. req.session.resetToken = query.passwordResetToken
  166. let emailQuery = ''
  167. if (typeof query.email === 'string') {
  168. const email = EmailsHelper.parseEmail(query.email)
  169. if (email) {
  170. emailQuery = `?email=${encodeURIComponent(email)}`
  171. }
  172. }
  173. return res.redirect('/user/password/set' + emailQuery)
  174. } catch (err) {
  175. if (err.name === 'ForbiddenError') {
  176. return next(err)
  177. }
  178. return res.redirect('/user/password/reset?error=token_expired')
  179. }
  180. }
  181. if (req.session.resetToken == null) {
  182. return res.redirect('/user/password/reset')
  183. }
  184. const email = EmailsHelper.parseEmail(query.email)
  185. // clean up to avoid leaking the token in the session object
  186. const passwordResetToken = req.session.resetToken
  187. delete req.session.resetToken
  188. res.render('user/setPassword', {
  189. title: 'set_password',
  190. email,
  191. passwordResetToken,
  192. })
  193. }
  194. const renderRequestResetFormSchema = z.object({
  195. query: z.object({
  196. error: z.string().optional(),
  197. }),
  198. })
  199. async function renderRequestResetForm(req, res) {
  200. const { query } = validateReq(req, renderRequestResetFormSchema)
  201. const errorQuery = query.error
  202. let error = null
  203. if (errorQuery === 'token_expired') {
  204. error = 'password_reset_token_expired'
  205. }
  206. res.render('user/passwordReset', {
  207. title: 'reset_password',
  208. error,
  209. })
  210. }
  211. export default {
  212. renderRequestResetForm: expressify(renderRequestResetForm),
  213. requestReset: expressify(requestReset),
  214. renderSetPasswordForm: expressify(renderSetPasswordForm),
  215. setNewUserPassword: expressify(setNewUserPassword),
  216. }