|
|
@@ -24,23 +24,19 @@ 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',
|
|
|
+const sendConfirmCodeRateLimiter = new RateLimiter('send-confirmation-code', {
|
|
|
+ points: 1,
|
|
|
+ duration: 60,
|
|
|
+})
|
|
|
+const checkConfirmCodeRateLimiter = new RateLimiter(
|
|
|
+ 'check-confirmation-code-per-email',
|
|
|
{
|
|
|
points: 10,
|
|
|
duration: 60,
|
|
|
}
|
|
|
)
|
|
|
-
|
|
|
-const resendSecondaryConfirmCodeRateLimiter = new RateLimiter(
|
|
|
- 'resend-secondary-confirmation-code',
|
|
|
+const resendConfirmCodeRateLimiter = new RateLimiter(
|
|
|
+ 'resend-confirmation-code',
|
|
|
{
|
|
|
points: 1,
|
|
|
duration: 60,
|
|
|
@@ -146,6 +142,23 @@ async function sendReconfirmation(req, res) {
|
|
|
res.sendStatus(204)
|
|
|
}
|
|
|
|
|
|
+async function sendExistingSecondaryEmailConfirmationCode(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,
|
|
|
+ email,
|
|
|
+ })
|
|
|
+ if (!user || user._id.toString() !== userId) {
|
|
|
+ return res.sendStatus(422)
|
|
|
+ }
|
|
|
+ await sendCodeAndStoreInSession(req, 'pendingExistingEmail', 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.
|
|
|
@@ -177,7 +190,7 @@ async function addWithConfirmationCode(req, res) {
|
|
|
try {
|
|
|
await UserGetter.promises.ensureUniqueEmailAddress(email)
|
|
|
|
|
|
- await sendSecondaryConfirmCodeRateLimiter.consume(email, 1, {
|
|
|
+ await sendConfirmCodeRateLimiter.consume(email, 1, {
|
|
|
method: 'email',
|
|
|
})
|
|
|
|
|
|
@@ -191,18 +204,12 @@ async function addWithConfirmationCode(req, res) {
|
|
|
}
|
|
|
)
|
|
|
|
|
|
- const { confirmCode, confirmCodeExpiresTimestamp } =
|
|
|
- await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
|
|
|
- email,
|
|
|
- true
|
|
|
- )
|
|
|
-
|
|
|
- req.session.pendingSecondaryEmail = {
|
|
|
+ await sendCodeAndStoreInSession(
|
|
|
+ req,
|
|
|
+ 'pendingSecondaryEmail',
|
|
|
email,
|
|
|
- confirmCode,
|
|
|
- confirmCodeExpiresTimestamp,
|
|
|
- affiliationOptions,
|
|
|
- }
|
|
|
+ affiliationOptions
|
|
|
+ )
|
|
|
|
|
|
return res.sendStatus(200)
|
|
|
} catch (err) {
|
|
|
@@ -231,186 +238,238 @@ async function addWithConfirmationCode(req, res) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-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'
|
|
|
+/**
|
|
|
+ * @param {import('express').Request} req
|
|
|
+ * @param {string} sessionKey
|
|
|
+ * @param {string} email
|
|
|
+ * @param affiliationOptions
|
|
|
+ * @returns {Promise<void>}
|
|
|
+ */
|
|
|
+async function sendCodeAndStoreInSession(
|
|
|
+ req,
|
|
|
+ sessionKey,
|
|
|
+ email,
|
|
|
+ affiliationOptions
|
|
|
+) {
|
|
|
+ const { confirmCode, confirmCodeExpiresTimestamp } =
|
|
|
+ await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
|
|
|
+ email,
|
|
|
+ false
|
|
|
)
|
|
|
+ req.session[sessionKey] = {
|
|
|
+ email,
|
|
|
+ confirmCode,
|
|
|
+ confirmCodeExpiresTimestamp,
|
|
|
+ affiliationOptions,
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- return res.status(500).json({
|
|
|
- message: {
|
|
|
- key: 'error_performing_request',
|
|
|
- },
|
|
|
+/**
|
|
|
+ * @param {string} sessionKey
|
|
|
+ * @param {(req: import('express').Request, user: any, email: string, affiliationOptions: any) => Promise<void>} beforeConfirmEmail
|
|
|
+ * @returns {Promise<*>}
|
|
|
+ */
|
|
|
+const _checkConfirmationCode =
|
|
|
+ (sessionKey, beforeConfirmEmail) => async (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,
|
|
|
})
|
|
|
- }
|
|
|
|
|
|
- const newSecondaryEmail = req.session.pendingSecondaryEmail.email
|
|
|
+ const sessionData = req.session[sessionKey]
|
|
|
|
|
|
- try {
|
|
|
- await checkSecondaryConfirmCodeRateLimiter.consume(newSecondaryEmail, 1, {
|
|
|
- method: 'email',
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- if (err?.remainingPoints === 0) {
|
|
|
- return res.sendStatus(429)
|
|
|
- } else {
|
|
|
- return res.status(500).json({
|
|
|
+ if (!sessionData) {
|
|
|
+ logger.err({}, `error checking confirmation code. missing ${sessionKey}`)
|
|
|
+
|
|
|
+ return res.status(422).json({
|
|
|
message: {
|
|
|
key: 'error_performing_request',
|
|
|
},
|
|
|
})
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (
|
|
|
- req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp < Date.now()
|
|
|
- ) {
|
|
|
- return res.status(403).json({
|
|
|
- message: { key: 'expired_confirmation_code' },
|
|
|
- })
|
|
|
- }
|
|
|
+ const emailToCheck = sessionData.email
|
|
|
|
|
|
- if (!tsscmp(req.session.pendingSecondaryEmail.confirmCode, code)) {
|
|
|
- return res.status(403).json({
|
|
|
- message: { key: 'invalid_confirmation_code' },
|
|
|
- })
|
|
|
+ try {
|
|
|
+ await checkConfirmCodeRateLimiter.consume(emailToCheck, 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 (sessionData.confirmCodeExpiresTimestamp < Date.now()) {
|
|
|
+ return res.status(403).json({
|
|
|
+ message: { key: 'expired_confirmation_code' },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!tsscmp(sessionData.confirmCode, code)) {
|
|
|
+ return res.status(403).json({
|
|
|
+ message: { key: 'invalid_confirmation_code' },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await beforeConfirmEmail(
|
|
|
+ req,
|
|
|
+ user,
|
|
|
+ emailToCheck,
|
|
|
+ sessionData.affiliationOptions
|
|
|
+ )
|
|
|
+
|
|
|
+ await UserUpdater.promises.confirmEmail(
|
|
|
+ userId,
|
|
|
+ emailToCheck,
|
|
|
+ sessionData.affiliationOptions
|
|
|
+ )
|
|
|
+
|
|
|
+ delete req.session[sessionKey]
|
|
|
+
|
|
|
+ AnalyticsManager.recordEventForUserInBackground(
|
|
|
+ user._id,
|
|
|
+ 'email-verified',
|
|
|
+ {
|
|
|
+ provider: 'email',
|
|
|
+ verification_type: 'token',
|
|
|
+ isPrimary: user.email === emailToCheck,
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ 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',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- try {
|
|
|
+const checkNewSecondaryEmailConfirmationCode = _checkConfirmationCode(
|
|
|
+ 'pendingSecondaryEmail',
|
|
|
+ async (req, user, email, affiliationOptions) => {
|
|
|
await UserAuditLogHandler.promises.addEntry(
|
|
|
- userId,
|
|
|
+ user._id,
|
|
|
'add-email-via-code',
|
|
|
- userId,
|
|
|
+ user._id,
|
|
|
req.ip,
|
|
|
- { newSecondaryEmail }
|
|
|
+ { newSecondaryEmail: email }
|
|
|
)
|
|
|
-
|
|
|
- await _sendSecurityAlertEmail(user, newSecondaryEmail)
|
|
|
-
|
|
|
+ await _sendSecurityAlertEmail(user, email)
|
|
|
await UserUpdater.promises.addEmailAddress(
|
|
|
- userId,
|
|
|
- newSecondaryEmail,
|
|
|
- req.session.pendingSecondaryEmail.affiliationOptions,
|
|
|
+ user._id,
|
|
|
+ email,
|
|
|
+ affiliationOptions,
|
|
|
{
|
|
|
initiatorId: user._id,
|
|
|
ipAddress: req.ip,
|
|
|
}
|
|
|
)
|
|
|
+ }
|
|
|
+)
|
|
|
|
|
|
- await UserUpdater.promises.confirmEmail(
|
|
|
- userId,
|
|
|
- newSecondaryEmail,
|
|
|
- req.session.pendingSecondaryEmail.affiliationOptions
|
|
|
- )
|
|
|
-
|
|
|
- delete req.session.pendingSecondaryEmail
|
|
|
-
|
|
|
- AnalyticsManager.recordEventForUserInBackground(
|
|
|
+const checkExistingEmailConfirmationCode = _checkConfirmationCode(
|
|
|
+ 'pendingExistingEmail',
|
|
|
+ async (req, user, email) => {
|
|
|
+ await UserAuditLogHandler.promises.addEntry(
|
|
|
user._id,
|
|
|
- 'email-verified',
|
|
|
- {
|
|
|
- provider: 'email',
|
|
|
- verification_type: 'token',
|
|
|
- isPrimary: false,
|
|
|
- }
|
|
|
+ 'confirm-email-via-code',
|
|
|
+ user._id,
|
|
|
+ req.ip,
|
|
|
+ { email }
|
|
|
)
|
|
|
+ }
|
|
|
+)
|
|
|
|
|
|
- const redirectUrl =
|
|
|
- AuthenticationController.getRedirectFromSession(req) || '/project'
|
|
|
-
|
|
|
- return res.json({
|
|
|
- redir: redirectUrl,
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- if (error.name === 'EmailExistsError') {
|
|
|
- return res.status(409).json({
|
|
|
+const _resendConfirmationCode =
|
|
|
+ (sessionKey, operation, auditLogEmailKey) => async (req, res) => {
|
|
|
+ const sessionData = req.session[sessionKey]
|
|
|
+ if (!sessionData) {
|
|
|
+ logger.err({}, `error resending confirmation code. missing ${sessionKey}`)
|
|
|
+ return res.status(422).json({
|
|
|
message: {
|
|
|
- type: 'error',
|
|
|
- text: req.i18n.translate('email_already_registered'),
|
|
|
+ key: 'error_performing_request',
|
|
|
},
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- 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'
|
|
|
- )
|
|
|
+ const email = sessionData.email
|
|
|
|
|
|
- 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 {
|
|
|
+ await resendConfirmCodeRateLimiter.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
|
|
|
+ try {
|
|
|
+ await UserAuditLogHandler.promises.addEntry(
|
|
|
+ userId,
|
|
|
+ operation,
|
|
|
+ userId,
|
|
|
+ req.ip,
|
|
|
+ { [auditLogEmailKey]: email }
|
|
|
)
|
|
|
|
|
|
- req.session.pendingSecondaryEmail.confirmCode = confirmCode
|
|
|
- req.session.pendingSecondaryEmail.confirmCodeExpiresTimestamp =
|
|
|
- confirmCodeExpiresTimestamp
|
|
|
+ const { confirmCode, confirmCodeExpiresTimestamp } =
|
|
|
+ await UserEmailsConfirmationHandler.promises.sendConfirmationCode(
|
|
|
+ email,
|
|
|
+ false
|
|
|
+ )
|
|
|
|
|
|
- return res.status(200).json({
|
|
|
- message: { key: 'we_sent_new_code' },
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- logger.err({ err, email }, 'failed to send confirmation code')
|
|
|
+ sessionData.confirmCode = confirmCode
|
|
|
+ sessionData.confirmCodeExpiresTimestamp = confirmCodeExpiresTimestamp
|
|
|
|
|
|
- return res.status(500).json({
|
|
|
- key: 'error_performing_request',
|
|
|
- })
|
|
|
+ return res.status(200).json({ message: { key: 'we_sent_new_code' } })
|
|
|
+ } catch (err) {
|
|
|
+ logger.err({ err, userId, email }, 'failed to send confirmation code')
|
|
|
+ return res.status(500).json({ key: 'error_performing_request' })
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
+
|
|
|
+const resendNewSecondaryEmailConfirmationCode = _resendConfirmationCode(
|
|
|
+ 'pendingSecondaryEmail',
|
|
|
+ 'resend-add-email-code',
|
|
|
+ 'newSecondaryEmail'
|
|
|
+)
|
|
|
+
|
|
|
+const resendExistingSecondaryEmailConfirmationCode = _resendConfirmationCode(
|
|
|
+ 'pendingExistingEmail',
|
|
|
+ 'resend-confirm-email-code',
|
|
|
+ 'email'
|
|
|
+)
|
|
|
|
|
|
async function confirmSecondaryEmailPage(req, res) {
|
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
|
@@ -623,12 +682,23 @@ const UserEmailsController = {
|
|
|
},
|
|
|
|
|
|
add: expressify(add),
|
|
|
+
|
|
|
addWithConfirmationCode: expressify(addWithConfirmationCode),
|
|
|
- checkSecondaryEmailConfirmationCode: expressify(
|
|
|
- checkSecondaryEmailConfirmationCode
|
|
|
+
|
|
|
+ checkNewSecondaryEmailConfirmationCode: expressify(
|
|
|
+ checkNewSecondaryEmailConfirmationCode
|
|
|
),
|
|
|
- resendSecondaryEmailConfirmationCode: expressify(
|
|
|
- resendSecondaryEmailConfirmationCode
|
|
|
+
|
|
|
+ checkExistingEmailConfirmationCode: expressify(
|
|
|
+ checkExistingEmailConfirmationCode
|
|
|
+ ),
|
|
|
+
|
|
|
+ resendNewSecondaryEmailConfirmationCode: expressify(
|
|
|
+ resendNewSecondaryEmailConfirmationCode
|
|
|
+ ),
|
|
|
+
|
|
|
+ resendExistingSecondaryEmailConfirmationCode: expressify(
|
|
|
+ resendExistingSecondaryEmailConfirmationCode
|
|
|
),
|
|
|
|
|
|
remove: expressify(remove),
|
|
|
@@ -660,6 +730,10 @@ const UserEmailsController = {
|
|
|
|
|
|
sendReconfirmation: expressify(sendReconfirmation),
|
|
|
|
|
|
+ sendExistingSecondaryEmailConfirmationCode: expressify(
|
|
|
+ sendExistingSecondaryEmailConfirmationCode
|
|
|
+ ),
|
|
|
+
|
|
|
addSecondaryEmailPage: expressify(addSecondaryEmailPage),
|
|
|
|
|
|
confirmSecondaryEmailPage: expressify(confirmSecondaryEmailPage),
|