From 33b28db0610e6549b355c0e3987502d818ee5d3c Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jul 2018 16:39:04 +0100 Subject: [PATCH] Add backend endpoint for resending confirmation email --- .../Security/OneTimeTokenHandler.coffee | 10 ++ .../User/UserEmailsConfirmationHandler.coffee | 11 ++ .../Features/User/UserEmailsController.coffee | 13 ++ services/web/app/coffee/router.coffee | 3 + .../acceptance/coffee/UserEmailsTests.coffee | 126 ++++++++++++++++-- 5 files changed, 154 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee index 69c9f5b0e9..dfae55e7cf 100644 --- a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee +++ b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee @@ -27,6 +27,16 @@ module.exports = return callback(error) if error? callback null, token + findValidTokenFromData: (use, data, callback = (error, token) ->) -> + db.tokens.findOne { + use: use, + data: data, + expiresAt: { $gt: new Date() }, + usedAt: { $exists: false } + }, (error, token) -> + return callback(error) if error? + return callback null, token?.token + getValueFromTokenAndExpire: (use, token, callback = (error, data) ->)-> logger.log token_start: token.slice(0,8), "getting data from #{use} token" now = new Date() diff --git a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee index dd87570450..53ae1fca2b 100644 --- a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee @@ -23,6 +23,17 @@ module.exports = UserEmailsConfirmationHandler = confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" EmailHandler.sendEmail emailTemplate, emailOptions, callback + resendConfirmationEmail: (user_id, email, callback = (error) ->) -> + OneTimeTokenHandler.findValidTokenFromData 'email_confirmation', { user_id, email }, (error, token) -> + return callback(error) if error? + if !token? + UserEmailsConfirmationHandler.sendConfirmationEmail user_id, email, callback + else + emailOptions = + to: email + confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" + EmailHandler.sendEmail 'confirmEmail', emailOptions, callback + confirmEmailFromToken: (token, callback = (error) ->) -> logger.log {token_start: token.slice(0,8)}, 'confirming email from token' OneTimeTokenHandler.getValueFromTokenAndExpire 'email_confirmation', token, (error, data) -> diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee index af1a355d6e..5c2fa5f587 100644 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ b/services/web/app/coffee/Features/User/UserEmailsController.coffee @@ -61,6 +61,19 @@ module.exports = UserEmailsController = return next(error) if error? res.sendStatus 204 + resendConfirmation: (req, res, next) -> + userId = AuthenticationController.getLoggedInUserId(req) + email = EmailHelper.parseEmail(req.body.email) + return res.sendStatus 422 unless email? + UserGetter.getUserByAnyEmail email, {_id:1}, (error, user) -> + return next(error) if error? + if !user? or user?._id?.toString() != userId + logger.log {userId, email, foundUserId: user?._id}, "email doesn't match logged in user" + return res.sendStatus 422 + logger.log {userId, email}, 'resending email confirmation token' + UserEmailsConfirmationHandler.resendConfirmationEmail userId, email, (error) -> + return next(error) if error? + res.sendStatus 200 showConfirm: (req, res, next) -> res.render 'user/confirm_email', { diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 4419dddac7..72a0ddebc4 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -115,6 +115,9 @@ module.exports = class Router UserEmailsController.showConfirm webRouter.post '/user/emails/confirm', UserEmailsController.confirm + webRouter.post '/user/emails/resend_confirmation', + AuthenticationController.requireLogin(), + UserEmailsController.resendConfirmation if Features.hasFeature 'affiliations' webRouter.post '/user/emails', diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee index 256c8a690f..40d0ca2244 100644 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ b/services/web/test/acceptance/coffee/UserEmailsTests.coffee @@ -17,7 +17,7 @@ describe "UserEmails", -> token = null async.series [ (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails', json: @@ -45,7 +45,7 @@ describe "UserEmails", -> token = tokens[0].token cb() (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails/confirm', json: @@ -80,7 +80,7 @@ describe "UserEmails", -> (cb) => @user2.login cb (cb) => # Create email for first user - @user.request { + @user.request { method: 'POST', url: '/user/emails', json: {@email} @@ -99,21 +99,21 @@ describe "UserEmails", -> cb() (cb) => # Delete the email from the first user - @user.request { + @user.request { method: 'POST', url: '/user/emails/delete', json: {@email} }, cb (cb) => # Create email for second user - @user2.request { + @user2.request { method: 'POST', url: '/user/emails', json: {@email} }, cb (cb) => # Original confirmation token should no longer work - @user.request { + @user.request { method: 'POST', url: '/user/emails/confirm', json: @@ -158,7 +158,7 @@ describe "UserEmails", -> token = null async.series [ (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails', json: @@ -183,12 +183,12 @@ describe "UserEmails", -> db.tokens.update { token: token }, { - $set: { + $set: { expiresAt: new Date(Date.now() - 1000000) } }, cb (cb) => - @user.request { + @user.request { method: 'POST', url: '/user/emails/confirm', json: @@ -198,3 +198,111 @@ describe "UserEmails", -> expect(response.statusCode).to.equal 404 cb() ], done + + describe 'resending the confirmation', -> + it 'should resend the existing token', (done) -> + token = null + async.series [ + (cb) => + @user.request { + method: 'POST', + url: '/user/emails', + json: + email: 'reconfirmation-email@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 204 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + # There should only be one confirmation token at the moment + expect(tokens.length).to.equal 1 + expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' + expect(tokens[0].data.user_id).to.equal @user._id + token = tokens[0].token + cb() + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: + email: 'reconfirmation-email@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + # There should still only be one confirmation token + expect(tokens.length).to.equal 1 + expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' + expect(tokens[0].data.user_id).to.equal @user._id + token = tokens[0].token + cb() + ], done + + it 'should create a new token if none exists', (done) -> + # This should only be for users that have sign up with their main + # emails before the confirmation system existed + token = null + async.series [ + (cb) => + db.tokens.remove { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, cb + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: + email: @user.email + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 200 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + # There should still only be one confirmation token + expect(tokens.length).to.equal 1 + expect(tokens[0].data.email).to.equal @user.email + expect(tokens[0].data.user_id).to.equal @user._id + token = tokens[0].token + cb() + ], done + + it "should not allow reconfirmation if the email doesn't match the user", (done) -> + token = null + async.series [ + (cb) => + @user.request { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: + email: 'non-matching-email@example.com' + }, (error, response, body) => + return done(error) if error? + expect(response.statusCode).to.equal 422 + cb() + (cb) => + db.tokens.find { + use: 'email_confirmation', + 'data.user_id': @user._id, + usedAt: { $exists: false } + }, (error, tokens) => + expect(tokens.length).to.equal 0 + cb() + ], done