Просмотр исходного кода

Add backend endpoint for resending confirmation email

James Allen 8 лет назад
Родитель
Сommit
33b28db061

+ 10 - 0
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()

+ 11 - 0
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) ->

+ 13 - 0
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', {

+ 3 - 0
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',

+ 117 - 9
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