mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #17947 from overleaf/dp-secondary-email-confirmation-code
Add endpoints for secondary email confirmation by code GitOrigin-RevId: c2829672fd9aeca457f76958d4922b9c95086f26
This commit is contained in:
parent
38c3c2338f
commit
c2448ff3d2
6 changed files with 727 additions and 6 deletions
|
@ -257,7 +257,9 @@ templates.confirmCode = NoCTAEmailTemplate({
|
|||
return 'Confirm your email address'
|
||||
},
|
||||
message(opts, isPlainText) {
|
||||
const msg = [
|
||||
const msg = opts.isSecondary
|
||||
? ['Use this 6-digit code to confirm your email address.']
|
||||
: [
|
||||
`Welcome to Overleaf! We're so glad you joined us.`,
|
||||
'Use this 6-digit confirmation code to finish your setup.',
|
||||
]
|
||||
|
|
|
@ -43,7 +43,7 @@ function sendConfirmationEmail(userId, email, emailTemplate, callback) {
|
|||
)
|
||||
}
|
||||
|
||||
async function sendConfirmationCode(email) {
|
||||
async function sendConfirmationCode(email, isSecondary) {
|
||||
if (!EmailHelper.parseEmail(email)) {
|
||||
throw new Error('invalid email')
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ async function sendConfirmationCode(email) {
|
|||
await EmailHandler.promises.sendEmail('confirmCode', {
|
||||
to: email,
|
||||
confirmCode,
|
||||
isSecondary,
|
||||
category: ['ConfirmEmail'],
|
||||
})
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
|
@ -15,9 +16,34 @@ 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 tsscmp = require('tsscmp')
|
||||
|
||||
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,
|
||||
|
@ -30,6 +56,10 @@ async function _sendSecurityAlertEmail(user, email) {
|
|||
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)
|
||||
|
@ -127,6 +157,263 @@ function sendReconfirmation(req, res, next) {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
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,
|
||||
}
|
||||
|
||||
return res.json({
|
||||
redir: '/user/emails/confirm-secondary',
|
||||
})
|
||||
} 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',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await checkSecondaryConfirmCodeRateLimiter.consume(
|
||||
req.session.pendingSecondaryEmail.email,
|
||||
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: req.session.pendingSecondaryEmail.email,
|
||||
}
|
||||
)
|
||||
|
||||
await UserUpdater.promises.addEmailAddress(
|
||||
userId,
|
||||
req.session.pendingSecondaryEmail.email,
|
||||
{},
|
||||
{
|
||||
initiatorId: user._id,
|
||||
ipAddress: req.ip,
|
||||
}
|
||||
)
|
||||
|
||||
await UserUpdater.promises.confirmEmail(
|
||||
userId,
|
||||
req.session.pendingSecondaryEmail.email,
|
||||
{}
|
||||
)
|
||||
|
||||
delete req.session.pendingSecondaryEmail
|
||||
|
||||
AnalyticsManager.recordEventForUser(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 primaryEmailCheckPage(req, res) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const user = await UserGetter.promises.getUser(userId, {
|
||||
|
@ -175,6 +462,13 @@ const UserEmailsController = {
|
|||
},
|
||||
|
||||
add: expressify(add),
|
||||
addWithConfirmationCode: expressify(addWithConfirmationCode),
|
||||
checkSecondaryEmailConfirmationCode: expressify(
|
||||
checkSecondaryEmailConfirmationCode
|
||||
),
|
||||
resendSecondaryEmailConfirmationCode: expressify(
|
||||
resendSecondaryEmailConfirmationCode
|
||||
),
|
||||
|
||||
remove(req, res, next) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
|
|
|
@ -366,6 +366,30 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
RateLimiterMiddleware.rateLimit(rateLimiters.endorseEmail),
|
||||
UserEmailsController.endorse
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/emails/secondary',
|
||||
AuthenticationController.requireLogin(),
|
||||
PermissionsController.requirePermission('add-secondary-email'),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.addEmail),
|
||||
UserEmailsController.addWithConfirmationCode
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/emails/confirm-secondary',
|
||||
AuthenticationController.requireLogin(),
|
||||
PermissionsController.requirePermission('add-secondary-email'),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.checkEmailConfirmationCode),
|
||||
UserEmailsController.checkSecondaryEmailConfirmationCode
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/emails/resend-secondary-confirmation',
|
||||
AuthenticationController.requireLogin(),
|
||||
PermissionsController.requirePermission('add-secondary-email'),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.resendConfirmationCode),
|
||||
UserEmailsController.resendSecondaryEmailConfirmationCode
|
||||
)
|
||||
}
|
||||
|
||||
webRouter.get(
|
||||
|
|
192
services/web/test/acceptance/src/AddSecondaryEmailTests.js
Normal file
192
services/web/test/acceptance/src/AddSecondaryEmailTests.js
Normal file
|
@ -0,0 +1,192 @@
|
|||
const { expect } = require('chai')
|
||||
const User = require('./helpers/User').promises
|
||||
const logger = require('@overleaf/logger')
|
||||
const sinon = require('sinon')
|
||||
const { db } = require('../../../app/src/infrastructure/mongodb')
|
||||
const Features = require('../../../app/src/infrastructure/Features')
|
||||
|
||||
describe('Add secondary email address confirmation code email', function () {
|
||||
let spy
|
||||
let user, user2, res, confirmCode
|
||||
|
||||
const extractConfirmCode = () => {
|
||||
const emailDebugLog = spy.args.find(
|
||||
([, msg]) => msg === 'Would send email if enabled.'
|
||||
)
|
||||
const emailConfirmSubject = emailDebugLog[0].options.subject
|
||||
return emailConfirmSubject.match(/\((\d{6})\)/)[1]
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
if (!Features.hasFeature('affiliations')) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
spy = sinon.spy(logger, 'debug')
|
||||
user = new User()
|
||||
await user.register()
|
||||
await user.login()
|
||||
spy.resetHistory()
|
||||
|
||||
res = await user.doRequest('POST', {
|
||||
json: {
|
||||
email: 'secondary@overleaf.com',
|
||||
},
|
||||
uri: `/user/emails/secondary`,
|
||||
})
|
||||
|
||||
confirmCode = extractConfirmCode()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
if (!Features.hasFeature('affiliations')) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
spy.restore()
|
||||
})
|
||||
|
||||
it('should redirect to confirm secondary email page', function () {
|
||||
expect(res.response.statusCode).to.equal(200)
|
||||
expect(res.body.redir).to.equal('/user/emails/confirm-secondary')
|
||||
expect(confirmCode.length).to.equal(6)
|
||||
})
|
||||
|
||||
describe('with a valid confirmation code', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await user.doRequest('POST', {
|
||||
json: {
|
||||
code: confirmCode,
|
||||
},
|
||||
uri: '/user/emails/confirm-secondary',
|
||||
})
|
||||
})
|
||||
|
||||
it('should redirect to /project', async function () {
|
||||
expect(this.result.response.statusCode).to.equal(200)
|
||||
expect(this.result.body.redir).to.equal('/project')
|
||||
})
|
||||
|
||||
it('the new email should be saved in mongo', async function () {
|
||||
const userInDb = await db.users.findOne(
|
||||
{ email: user.email },
|
||||
{ projection: { emails: 1 } }
|
||||
)
|
||||
expect(userInDb).to.exist
|
||||
const newSecondaryEmail = userInDb.emails.find(
|
||||
email => email.email === 'secondary@overleaf.com'
|
||||
)
|
||||
expect(newSecondaryEmail).to.exist
|
||||
expect(newSecondaryEmail.confirmedAt).to.exist
|
||||
expect(newSecondaryEmail.reconfirmedAt).to.exist
|
||||
expect(newSecondaryEmail.reconfirmedAt).to.deep.equal(
|
||||
newSecondaryEmail.confirmedAt
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid confirmation code', function () {
|
||||
beforeEach(async function () {
|
||||
this.result = await user.doRequest('POST', {
|
||||
json: {
|
||||
code: '123',
|
||||
},
|
||||
uri: '/user/emails/confirm-secondary',
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with invalid confirmation code error', async function () {
|
||||
expect(this.result.response.statusCode).to.equal(403)
|
||||
expect(this.result.body.message.key).to.equal('invalid_confirmation_code')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a duplicate email', async function () {
|
||||
beforeEach(async function () {
|
||||
await user.doRequest('POST', {
|
||||
json: {
|
||||
code: confirmCode,
|
||||
},
|
||||
uri: '/user/emails/confirm-secondary',
|
||||
})
|
||||
|
||||
user2 = new User()
|
||||
await user2.register()
|
||||
await user2.login()
|
||||
})
|
||||
|
||||
it('should respond with a email already registered error', async function () {
|
||||
res = await user2.doRequest('POST', {
|
||||
json: {
|
||||
email: 'secondary@overleaf.com',
|
||||
},
|
||||
uri: `/user/emails/secondary`,
|
||||
})
|
||||
|
||||
expect(res.response.statusCode).to.equal(409)
|
||||
expect(res.body.message.text).to.equal('This email is already registered')
|
||||
})
|
||||
})
|
||||
|
||||
it('should hit rate limit on code check', async function () {
|
||||
let confirmEmailReq
|
||||
for (let i = 0; i < 20; i++) {
|
||||
confirmEmailReq = await user.doRequest('POST', {
|
||||
json: {
|
||||
code: '123',
|
||||
},
|
||||
uri: '/user/emails/confirm-secondary',
|
||||
})
|
||||
}
|
||||
|
||||
expect(confirmEmailReq.response.statusCode).to.equal(429)
|
||||
})
|
||||
|
||||
it('should resend confirm code', async function () {
|
||||
const oldConfirmCode = extractConfirmCode()
|
||||
spy.resetHistory()
|
||||
|
||||
const resendCodeRes = await user.doRequest('POST', {
|
||||
uri: '/user/emails/resend-secondary-confirmation',
|
||||
})
|
||||
|
||||
const newConfirmCode = extractConfirmCode()
|
||||
|
||||
expect(resendCodeRes.response.statusCode).to.equal(200)
|
||||
expect(JSON.parse(resendCodeRes.body).message.key).to.equal(
|
||||
'we_sent_new_code'
|
||||
)
|
||||
|
||||
const oldConfirmRes = await user.doRequest('POST', {
|
||||
json: {
|
||||
code: oldConfirmCode,
|
||||
},
|
||||
uri: '/user/emails/confirm-secondary',
|
||||
})
|
||||
|
||||
expect(oldConfirmRes.response.statusCode).to.equal(403)
|
||||
expect(oldConfirmRes.body.message.key).to.equal('invalid_confirmation_code')
|
||||
|
||||
const newCodeRes = await user.doRequest('POST', {
|
||||
json: {
|
||||
code: newConfirmCode,
|
||||
},
|
||||
uri: '/user/emails/confirm-secondary',
|
||||
})
|
||||
|
||||
expect(newCodeRes.response.statusCode).to.equal(200)
|
||||
expect(newCodeRes.body.redir).to.equal('/project')
|
||||
})
|
||||
|
||||
it('should hit rate limit on code resend', async function () {
|
||||
let resendCodeReq
|
||||
for (let i = 0; i < 5; i++) {
|
||||
resendCodeReq = await user.doRequest('POST', {
|
||||
json: true,
|
||||
uri: '/user/emails/resend-secondary-confirmation',
|
||||
})
|
||||
}
|
||||
|
||||
expect(resendCodeReq.response.statusCode).to.equal(429)
|
||||
})
|
||||
})
|
|
@ -17,7 +17,7 @@ describe('UserEmailsController', function () {
|
|||
this.user = {
|
||||
_id: 'mock-user-id',
|
||||
email: 'example@overleaf.com',
|
||||
emails: {},
|
||||
emails: [],
|
||||
}
|
||||
|
||||
this.UserGetter = {
|
||||
|
@ -25,6 +25,7 @@ describe('UserEmailsController', function () {
|
|||
getUserFullEmails: sinon.stub(),
|
||||
getUserByAnyEmail: sinon.stub(),
|
||||
promises: {
|
||||
ensureUniqueEmailAddress: sinon.stub().resolves(),
|
||||
getUser: sinon.stub().resolves(this.user),
|
||||
},
|
||||
}
|
||||
|
@ -46,6 +47,7 @@ describe('UserEmailsController', function () {
|
|||
updateV1AndSetDefaultEmailAddress: sinon.stub(),
|
||||
promises: {
|
||||
addEmailAddress: sinon.stub().resolves(),
|
||||
confirmEmail: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.EmailHelper = { parseEmail: sinon.stub() }
|
||||
|
@ -59,9 +61,23 @@ describe('UserEmailsController', function () {
|
|||
}
|
||||
this.UserAuditLogHandler = {
|
||||
addEntry: sinon.stub().yields(),
|
||||
promises: {
|
||||
addEntry: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.rateLimiter = {
|
||||
consume: sinon.stub().resolves(),
|
||||
}
|
||||
this.RateLimiter = {
|
||||
RateLimiter: sinon.stub().returns(this.rateLimiter),
|
||||
}
|
||||
this.AuthenticationController = {
|
||||
getRedirectFromSession: sinon.stub().returns(null),
|
||||
}
|
||||
this.UserEmailsController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../Authentication/AuthenticationController':
|
||||
this.AuthenticationController,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'../../infrastructure/Features': this.Features,
|
||||
'./UserSessionsManager': this.UserSessionsManager,
|
||||
|
@ -84,6 +100,7 @@ describe('UserEmailsController', function () {
|
|||
'../Errors/HttpErrorHandler': this.HttpErrorHandler,
|
||||
'../Analytics/AnalyticsManager': this.AnalyticsManager,
|
||||
'./UserAuditLogHandler': this.UserAuditLogHandler,
|
||||
'../../infrastructure/RateLimiter': this.RateLimiter,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -267,6 +284,196 @@ describe('UserEmailsController', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('addWithConfirmationCode', function () {
|
||||
beforeEach(function () {
|
||||
this.newEmail = 'new_email@baz.com'
|
||||
this.req.body = {
|
||||
email: this.newEmail,
|
||||
}
|
||||
this.EmailHelper.parseEmail.returns(this.newEmail)
|
||||
this.UserEmailsConfirmationHandler.promises.sendConfirmationCode = sinon
|
||||
.stub()
|
||||
.resolves({
|
||||
confirmCode: '123456',
|
||||
confirmCodeExpiresTimestamp: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('sends an email confirmation', function (done) {
|
||||
this.UserEmailsController.addWithConfirmationCode(this.req, {
|
||||
json: ({ redir }) => {
|
||||
redir.should.equal('/user/emails/confirm-secondary')
|
||||
assertCalledWith(
|
||||
this.UserEmailsConfirmationHandler.promises.sendConfirmationCode,
|
||||
this.newEmail,
|
||||
true
|
||||
)
|
||||
done()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('handles email parse error', function (done) {
|
||||
this.EmailHelper.parseEmail.returns(null)
|
||||
this.UserEmailsController.addWithConfirmationCode(this.req, {
|
||||
sendStatus: code => {
|
||||
code.should.equal(422)
|
||||
done()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('handles when the email already exists', function (done) {
|
||||
this.UserGetter.promises.ensureUniqueEmailAddress.rejects(
|
||||
new Errors.EmailExistsError()
|
||||
)
|
||||
|
||||
this.UserEmailsController.addWithConfirmationCode(this.req, {
|
||||
status: code => {
|
||||
code.should.equal(409)
|
||||
return { json: () => done() }
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail to add new emails when the limit has been reached', function (done) {
|
||||
this.user.emails = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.user.emails.push({ email: `example${i}@overleaf.com` })
|
||||
}
|
||||
this.UserEmailsController.addWithConfirmationCode(this.req, {
|
||||
status: code => {
|
||||
expect(code).to.equal(422)
|
||||
return {
|
||||
json: error => {
|
||||
expect(error.message).to.equal('secondary email limit exceeded')
|
||||
done()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkSecondaryEmailConfirmationCode', function () {
|
||||
beforeEach(function () {
|
||||
this.newEmail = 'new_email@baz.com'
|
||||
this.req.session.pendingSecondaryEmail = {
|
||||
confirmCode: '123456',
|
||||
email: this.newEmail,
|
||||
confirmCodeExpiresTimestamp: new Date(Math.max),
|
||||
}
|
||||
})
|
||||
|
||||
describe('with a valid confirmation code', function () {
|
||||
beforeEach(function () {
|
||||
this.req.body = {
|
||||
code: '123456',
|
||||
}
|
||||
})
|
||||
|
||||
it('adds the email', function (done) {
|
||||
this.UserEmailsController.checkSecondaryEmailConfirmationCode(
|
||||
this.req,
|
||||
{
|
||||
json: () => {
|
||||
assertCalledWith(
|
||||
this.UserUpdater.promises.addEmailAddress,
|
||||
this.user._id,
|
||||
this.newEmail
|
||||
)
|
||||
assertCalledWith(
|
||||
this.UserUpdater.promises.confirmEmail,
|
||||
this.user._id,
|
||||
this.newEmail
|
||||
)
|
||||
done()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects to /project', function (done) {
|
||||
this.UserEmailsController.checkSecondaryEmailConfirmationCode(
|
||||
this.req,
|
||||
{
|
||||
json: ({ redir }) => {
|
||||
redir.should.equal('/project')
|
||||
done()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid confirmation code', function () {
|
||||
beforeEach(function () {
|
||||
this.req.body = {
|
||||
code: '999999',
|
||||
}
|
||||
})
|
||||
|
||||
it('does not add the email', function (done) {
|
||||
this.UserEmailsController.checkSecondaryEmailConfirmationCode(
|
||||
this.req,
|
||||
{
|
||||
status: () => {
|
||||
assertNotCalled(this.UserUpdater.promises.addEmailAddress)
|
||||
assertNotCalled(this.UserUpdater.promises.confirmEmail)
|
||||
done()
|
||||
return { json: this.next }
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with a 403', function (done) {
|
||||
this.UserEmailsController.checkSecondaryEmailConfirmationCode(
|
||||
this.req,
|
||||
{
|
||||
status: code => {
|
||||
code.should.equal(403)
|
||||
done()
|
||||
return { json: this.next }
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resendSecondaryEmailConfirmationCode', function () {
|
||||
beforeEach(function () {
|
||||
this.newEmail = 'new_email@baz.com'
|
||||
this.req.session.pendingSecondaryEmail = {
|
||||
confirmCode: '123456',
|
||||
email: this.newEmail,
|
||||
confirmCodeExpiresTimestamp: new Date(Math.max),
|
||||
}
|
||||
this.UserEmailsConfirmationHandler.promises.sendConfirmationCode = sinon
|
||||
.stub()
|
||||
.resolves({
|
||||
confirmCode: '123456',
|
||||
confirmCodeExpiresTimestamp: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should send the email', function (done) {
|
||||
this.UserEmailsController.resendSecondaryEmailConfirmationCode(this.req, {
|
||||
status: code => {
|
||||
code.should.equal(200)
|
||||
assertCalledWith(
|
||||
this.UserEmailsConfirmationHandler.promises.sendConfirmationCode,
|
||||
this.newEmail,
|
||||
true
|
||||
)
|
||||
done()
|
||||
return { json: this.next }
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove', function () {
|
||||
beforeEach(function () {
|
||||
this.email = 'email_to_remove@bar.com'
|
||||
|
@ -541,6 +748,7 @@ describe('UserEmailsController', function () {
|
|||
expect(this.res.sendStatus.lastCall.args[0]).to.equal(422)
|
||||
done()
|
||||
})
|
||||
|
||||
describe('email on another user account', function () {
|
||||
beforeEach(function () {
|
||||
this.UserGetter.getUserByAnyEmail.yields(undefined, {
|
||||
|
|
Loading…
Reference in a new issue