| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 |
- const AuthenticationController = require('../Authentication/AuthenticationController')
- const Settings = require('@overleaf/settings')
- const logger = require('@overleaf/logger')
- const SessionManager = require('../Authentication/SessionManager')
- const UserGetter = require('./UserGetter')
- const UserUpdater = require('./UserUpdater')
- const UserSessionsManager = require('./UserSessionsManager')
- const EmailHandler = require('../Email/EmailHandler')
- const EmailHelper = require('../Helpers/EmailHelper')
- const UserEmailsConfirmationHandler = require('./UserEmailsConfirmationHandler')
- const { endorseAffiliation } = require('../Institutions/InstitutionsAPI')
- const Errors = require('../Errors/Errors')
- const HttpErrorHandler = require('../Errors/HttpErrorHandler')
- const { expressify } = require('@overleaf/promise-utils')
- const AsyncFormHelper = require('../Helpers/AsyncFormHelper')
- const AnalyticsManager = require('../Analytics/AnalyticsManager')
- const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
- const UserAuditLogHandler = require('./UserAuditLogHandler')
- const { RateLimiter } = require('../../infrastructure/RateLimiter')
- const Features = require('../../infrastructure/Features')
- const tsscmp = require('tsscmp')
- const Modules = require('../../infrastructure/Modules')
- const SplitTestHandler = require('../SplitTests/SplitTestHandler')
- const AUDIT_LOG_TOKEN_PREFIX_LENGTH = 10
- const sendSecondaryConfirmCodeRateLimiter = new RateLimiter(
- 'send-secondary-confirmation-code',
- {
- points: 1,
- duration: 60,
- }
- )
- const checkSecondaryConfirmCodeRateLimiter = new RateLimiter(
- 'check-secondary-confirmation-code-per-email',
- {
- points: 10,
- duration: 60,
- }
- )
- const resendSecondaryConfirmCodeRateLimiter = new RateLimiter(
- 'resend-secondary-confirmation-code',
- {
- points: 1,
- duration: 60,
- }
- )
- async function _sendSecurityAlertEmail(user, email) {
- const emailOptions = {
- to: user.email,
- actionDescribed: `a secondary email address has been added to your account ${user.email}`,
- message: [
- `<span style="display:inline-block;padding: 0 20px;width:100%;">Added: <br/><b>${email}</b></span>`,
- ],
- action: 'secondary email address added',
- }
- await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
- }
- /**
- * This method is for adding a secondary email to be confirmed via an emailed link.
- * For code confirmation, see the `addWithConfirmationCode` method in this file.
- */
- async function add(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- if (!email) {
- return res.sendStatus(422)
- }
- const user = await UserGetter.promises.getUser(userId, {
- email: 1,
- 'emails.email': 1,
- })
- if (user.emails.length >= Settings.emailAddressLimit) {
- return res.status(422).json({ message: 'secondary email limit exceeded' })
- }
- const affiliationOptions = {
- university: req.body.university,
- role: req.body.role,
- department: req.body.department,
- }
- try {
- await UserUpdater.promises.addEmailAddress(
- userId,
- email,
- affiliationOptions,
- {
- initiatorId: user._id,
- ipAddress: req.ip,
- }
- )
- } catch (error) {
- return UserEmailsController._handleEmailError(error, req, res, next)
- }
- await _sendSecurityAlertEmail(user, email)
- await UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
- userId,
- email
- )
- res.sendStatus(204)
- }
- async function resendConfirmation(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- if (!email) {
- return res.sendStatus(422)
- }
- const user = await UserGetter.promises.getUserByAnyEmail(email, { _id: 1 })
- if (!user || user._id.toString() !== userId) {
- return res.sendStatus(422)
- }
- await UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
- userId,
- email
- )
- res.sendStatus(200)
- }
- async function sendReconfirmation(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- if (!email) {
- return res.sendStatus(400)
- }
- const user = await UserGetter.promises.getUserByAnyEmail(email, { _id: 1 })
- if (!user || user._id.toString() !== userId) {
- return res.sendStatus(422)
- }
- await UserEmailsConfirmationHandler.promises.sendReconfirmationEmail(
- userId,
- email
- )
- res.sendStatus(204)
- }
- /**
- * This method is for adding a secondary email to be confirmed via a code.
- * For email link confirmation see the `add` method in this file.
- */
- async function addWithConfirmationCode(req, res) {
- delete req.session.pendingSecondaryEmail
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- const affiliationOptions = {
- university: req.body.university,
- role: req.body.role,
- department: req.body.department,
- }
- if (!email) {
- return res.sendStatus(422)
- }
- const user = await UserGetter.promises.getUser(userId, {
- email: 1,
- 'emails.email': 1,
- })
- if (user.emails.length >= Settings.emailAddressLimit) {
- return res.status(422).json({ message: 'secondary email limit exceeded' })
- }
- try {
- await UserGetter.promises.ensureUniqueEmailAddress(email)
- await sendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
- method: 'email',
- })
- await UserAuditLogHandler.promises.addEntry(
- userId,
- 'request-add-email-code',
- userId,
- req.ip,
- {
- newSecondaryEmail: email,
- }
- )
- const { confirmCode, confirmCodeExpiresTimestamp } =
- await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
- email,
- true
- )
- req.session.pendingSecondaryEmail = {
- email,
- confirmCode,
- confirmCodeExpiresTimestamp,
- affiliationOptions,
- }
- return res.sendStatus(200)
- } catch (err) {
- if (err.name === 'EmailExistsError') {
- return res.status(409).json({
- message: {
- type: 'error',
- text: req.i18n.translate('email_already_registered'),
- },
- })
- }
- if (err?.remainingPoints === 0) {
- return res.status(429).json({})
- }
- logger.err({ err }, 'failed to send confirmation code')
- delete req.session.pendingSecondaryEmail
- return res.status(500).json({
- message: {
- key: 'error_performing_request',
- },
- })
- }
- }
- async function checkSecondaryEmailConfirmationCode(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const code = req.body.code
- const user = await UserGetter.promises.getUser(userId, {
- email: 1,
- 'emails.email': 1,
- })
- if (!req.session.pendingSecondaryEmail) {
- logger.err(
- {},
- 'error checking confirmation code. missing pendingSecondaryEmail'
- )
- return res.status(500).json({
- message: {
- key: 'error_performing_request',
- },
- })
- }
- const newSecondaryEmail = req.session.pendingSecondaryEmail.email
- try {
- await checkSecondaryConfirmCodeRateLimiter.consume(newSecondaryEmail, 1, {
- method: 'email',
- })
- } catch (err) {
- if (err?.remainingPoints === 0) {
- return res.sendStatus(429)
- } else {
- return res.status(500).json({
- message: {
- key: 'error_performing_request',
- },
- })
- }
- }
- if (
- req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp < Date.now()
- ) {
- return res.status(403).json({
- message: { key: 'expired_confirmation_code' },
- })
- }
- if (!tsscmp(req.session.pendingSecondaryEmail.confirmCode, code)) {
- return res.status(403).json({
- message: { key: 'invalid_confirmation_code' },
- })
- }
- try {
- await UserAuditLogHandler.promises.addEntry(
- userId,
- 'add-email-via-code',
- userId,
- req.ip,
- { newSecondaryEmail }
- )
- await _sendSecurityAlertEmail(user, newSecondaryEmail)
- await UserUpdater.promises.addEmailAddress(
- userId,
- newSecondaryEmail,
- req.session.pendingSecondaryEmail.affiliationOptions,
- {
- initiatorId: user._id,
- ipAddress: req.ip,
- }
- )
- await UserUpdater.promises.confirmEmail(
- userId,
- newSecondaryEmail,
- req.session.pendingSecondaryEmail.affiliationOptions
- )
- delete req.session.pendingSecondaryEmail
- AnalyticsManager.recordEventForUserInBackground(
- user._id,
- 'email-verified',
- {
- provider: 'email',
- verification_type: 'token',
- isPrimary: false,
- }
- )
- const redirectUrl =
- AuthenticationController.getRedirectFromSession(req) || '/project'
- return res.json({
- redir: redirectUrl,
- })
- } catch (error) {
- if (error.name === 'EmailExistsError') {
- return res.status(409).json({
- message: {
- type: 'error',
- text: req.i18n.translate('email_already_registered'),
- },
- })
- }
- logger.err({ error }, 'failed to check confirmation code')
- return res.status(500).json({
- message: {
- key: 'error_performing_request',
- },
- })
- }
- }
- async function resendSecondaryEmailConfirmationCode(req, res) {
- if (!req.session.pendingSecondaryEmail) {
- logger.err(
- {},
- 'error resending confirmation code. missing pendingSecondaryEmail'
- )
- return res.status(500).json({
- message: {
- key: 'error_performing_request',
- },
- })
- }
- const email = req.session.pendingSecondaryEmail.email
- try {
- await resendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
- method: 'email',
- })
- } catch (err) {
- if (err?.remainingPoints === 0) {
- return res.status(429).json({})
- } else {
- throw err
- }
- }
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- await UserAuditLogHandler.promises.addEntry(
- userId,
- 'resend-add-email-code',
- userId,
- req.ip,
- {
- newSecondaryEmail: email,
- }
- )
- const { confirmCode, confirmCodeExpiresTimestamp } =
- await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
- email,
- true
- )
- req.session.pendingSecondaryEmail.confirmCode = confirmCode
- req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp =
- confirmCodeExpiresTimestamp
- return res.status(200).json({
- message: { key: 'we_sent_new_code' },
- })
- } catch (err) {
- logger.err({ err, email }, 'failed to send confirmation code')
- return res.status(500).json({
- key: 'error_performing_request',
- })
- }
- }
- async function confirmSecondaryEmailPage(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- if (!req.session.pendingSecondaryEmail) {
- const redirectURL =
- AuthenticationController.getRedirectFromSession(req) || '/project'
- return res.redirect(redirectURL)
- }
- // Populates splitTestVariants with a value for the split test name and allows
- // Pug to read it
- await SplitTestHandler.promises.getAssignment(req, res, 'misc-b2c-pages-bs5')
- AnalyticsManager.recordEventForUserInBackground(
- userId,
- 'confirm-secondary-email-page-displayed'
- )
- res.render('user/confirmSecondaryEmail', {
- email: req.session.pendingSecondaryEmail.email,
- })
- }
- async function addSecondaryEmailPage(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const confirmedEmails =
- await UserGetter.promises.getUserConfirmedEmails(userId)
- if (confirmedEmails.length >= 2) {
- const redirectURL =
- AuthenticationController.getRedirectFromSession(req) || '/project'
- return res.redirect(redirectURL)
- }
- AnalyticsManager.recordEventForUserInBackground(
- userId,
- 'add-secondary-email-page-displayed'
- )
- res.render('user/addSecondaryEmail')
- }
- async function primaryEmailCheckPage(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const user = await UserGetter.promises.getUser(userId, {
- lastPrimaryEmailCheck: 1,
- signUpDate: 1,
- email: 1,
- emails: 1,
- })
- if (!UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)) {
- return res.redirect('/project')
- }
- AnalyticsManager.recordEventForUserInBackground(
- userId,
- 'primary-email-check-page-displayed'
- )
- const { variant } = await SplitTestHandler.promises.getAssignment(
- req,
- res,
- 'auth-pages-bs5'
- )
- const template =
- variant === 'enabled'
- ? 'user/primaryEmailCheck-bs5'
- : 'user/primaryEmailCheck'
- res.render(template)
- }
- async function primaryEmailCheck(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- await UserUpdater.promises.updateUser(userId, {
- $set: { lastPrimaryEmailCheck: new Date() },
- })
- AnalyticsManager.recordEventForUserInBackground(
- userId,
- 'primary-email-check-done'
- )
- // We want to redirect to prompt a user to add a secondary email if their primary
- // is an institutional email and they dont' already have a secondary.
- if (Features.hasFeature('saas') && req.capabilitySet.has('add-affiliation')) {
- const confirmedEmails =
- await UserGetter.promises.getUserConfirmedEmails(userId)
- if (confirmedEmails.length < 2) {
- const { email: primaryEmail } = SessionManager.getSessionUser(req.session)
- const primaryEmailDomain = EmailHelper.getDomain(primaryEmail)
- const institution = (
- await Modules.promises.hooks.fire(
- 'getInstitutionViaDomain',
- primaryEmailDomain
- )
- )?.[0]
- if (institution) {
- return AsyncFormHelper.redirect(req, res, '/user/emails/add-secondary')
- }
- }
- }
- AsyncFormHelper.redirect(req, res, '/project')
- }
- async function showConfirm(req, res, next) {
- res.render('user/confirm_email', {
- token: req.query.token,
- title: 'confirm_email',
- })
- }
- async function remove(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- if (!email) {
- return res.sendStatus(422)
- }
- const auditLog = {
- initiatorId: userId,
- ipAddress: req.ip,
- }
- await UserUpdater.promises.removeEmailAddress(userId, email, auditLog)
- res.sendStatus(200)
- }
- const UserEmailsController = {
- list(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- UserGetter.getUserFullEmails(userId, function (error, fullEmails) {
- if (error) {
- return next(error)
- }
- res.json(fullEmails)
- })
- },
- add: expressify(add),
- addWithConfirmationCode: expressify(addWithConfirmationCode),
- checkSecondaryEmailConfirmationCode: expressify(
- checkSecondaryEmailConfirmationCode
- ),
- resendSecondaryEmailConfirmationCode: expressify(
- resendSecondaryEmailConfirmationCode
- ),
- remove: expressify(remove),
- setDefault(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- if (!email) {
- return res.sendStatus(422)
- }
- const auditLog = {
- initiatorId: userId,
- ipAddress: req.ip,
- }
- UserUpdater.setDefaultEmailAddress(
- userId,
- email,
- false,
- auditLog,
- true,
- err => {
- if (err) {
- return UserEmailsController._handleEmailError(err, req, res, next)
- }
- SessionManager.setInSessionUser(req.session, { email })
- const user = SessionManager.getSessionUser(req.session)
- UserSessionsManager.removeSessionsFromRedis(
- user,
- req.sessionID, // remove all sessions except the current session
- err => {
- if (err)
- logger.warn(
- { err },
- 'failed revoking secondary sessions after changing default email'
- )
- }
- )
- res.sendStatus(200)
- }
- )
- },
- endorse(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const email = EmailHelper.parseEmail(req.body.email)
- if (!email) {
- return res.sendStatus(422)
- }
- endorseAffiliation(
- userId,
- email,
- req.body.role,
- req.body.department,
- function (error) {
- if (error) {
- return next(error)
- }
- res.sendStatus(204)
- }
- )
- },
- resendConfirmation: expressify(resendConfirmation),
- sendReconfirmation: expressify(sendReconfirmation),
- addSecondaryEmailPage: expressify(addSecondaryEmailPage),
- confirmSecondaryEmailPage: expressify(confirmSecondaryEmailPage),
- primaryEmailCheckPage: expressify(primaryEmailCheckPage),
- primaryEmailCheck: expressify(primaryEmailCheck),
- showConfirm: expressify(showConfirm),
- confirm(req, res, next) {
- const { token } = req.body
- if (!token) {
- return res.status(422).json({
- message: req.i18n.translate('confirmation_link_broken'),
- })
- }
- UserEmailsConfirmationHandler.confirmEmailFromToken(
- req,
- token,
- function (error, userData) {
- if (error) {
- if (error instanceof Errors.ForbiddenError) {
- res.status(403).json({
- message: {
- key: 'confirm-email-wrong-user',
- text: `We can’t confirm this email. You must be logged in with the Overleaf account that requested the new secondary email.`,
- },
- })
- } else if (error instanceof Errors.NotFoundError) {
- res.status(404).json({
- message: req.i18n.translate('confirmation_token_invalid'),
- })
- } else {
- next(error)
- }
- } else {
- const { userId, email } = userData
- const tokenPrefix = token.substring(0, AUDIT_LOG_TOKEN_PREFIX_LENGTH)
- UserAuditLogHandler.addEntry(
- userId,
- 'confirm-email',
- userId,
- req.ip,
- { token: tokenPrefix, email },
- auditLogError => {
- if (auditLogError) {
- logger.error(
- { error: auditLogError, userId, token: tokenPrefix },
- 'failed to add audit log entry'
- )
- }
- UserGetter.getUser(
- userData.userId,
- { email: 1 },
- function (error, user) {
- if (error) {
- logger.error(
- { error, userId: userData.userId },
- 'failed to get user'
- )
- }
- const isPrimary = user?.email === userData.email
- AnalyticsManager.recordEventForUserInBackground(
- userData.userId,
- 'email-verified',
- {
- provider: 'email',
- verification_type: 'link',
- isPrimary,
- }
- )
- res.sendStatus(200)
- }
- )
- }
- )
- }
- }
- )
- },
- _handleEmailError(error, req, res, next) {
- if (error instanceof Errors.UnconfirmedEmailError) {
- return HttpErrorHandler.conflict(req, res, 'email must be confirmed')
- } else if (error instanceof Errors.EmailExistsError) {
- const message = req.i18n.translate('email_already_registered')
- return HttpErrorHandler.conflict(req, res, message)
- } else if (error.message === '422: Email does not belong to university') {
- const message = req.i18n.translate('email_does_not_belong_to_university')
- return HttpErrorHandler.conflict(req, res, message)
- }
- next(error)
- },
- }
- module.exports = UserEmailsController
|