From 22ba39812be84d39f8d46fbdcb6213aeeb06df6c Mon Sep 17 00:00:00 2001 From: cmmills91 Date: Wed, 10 May 2017 14:02:16 +0100 Subject: [PATCH] Add support for resending verification email in case of expired token (#3617) * -Defines new public API route /apps/:appId/resend_verification_email that will generate a new email verification link and email for a user identified by username in POST body -Add template and url support for invalidVerificationLink, linkSendSuccess, and linkSendFail pages. The invalidVerificationLink pages includes a button that allows the user to generate a new verification email if their current token has expired, using the new public API route -All three pages have default html that will be functional out of the box, but they can be customized in the customPages object. The custom page for invalidVerificationLink needs to handle the extraction of the username and appId from the url and the POST to generate the new link (this requires javascript) -Clicking a link for an email that has already been verified now routes to the emailVerifySuccess page instead of the invalidLink page * Fix package.json repo url to be parse-server againwq * Fix js lint issues * Update unit tests * Use arrow functions, change html page comments, use qs and a string template to construct location for invalidVerificationLink page, syntax fixes * Remember to pass result when using arrow function --- public_html/invalid_link.html | 2 + public_html/invalid_verification_link.html | 68 ++++++++++++++++++++++ public_html/link_send_fail.html | 45 ++++++++++++++ public_html/link_send_success.html | 45 ++++++++++++++ spec/EmailVerificationToken.spec.js | 12 ++-- spec/ValidationAndPasswordsReset.spec.js | 31 +++++++++- src/Config.js | 12 ++++ src/Controllers/UserController.js | 26 +++++++-- src/Routers/PublicAPIRouter.js | 45 ++++++++++++++ 9 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 public_html/invalid_verification_link.html create mode 100644 public_html/link_send_fail.html create mode 100644 public_html/link_send_success.html diff --git a/public_html/invalid_link.html b/public_html/invalid_link.html index 66bdc788..b19044e5 100644 --- a/public_html/invalid_link.html +++ b/public_html/invalid_link.html @@ -35,6 +35,8 @@ padding: 0 0 0 0; } + +

Invalid Link

diff --git a/public_html/invalid_verification_link.html b/public_html/invalid_verification_link.html new file mode 100644 index 00000000..fe6914fc --- /dev/null +++ b/public_html/invalid_verification_link.html @@ -0,0 +1,68 @@ + + + + + Invalid Link + + + + + +
+

Invalid Verification Link

+
+ + +
+
+ + diff --git a/public_html/link_send_fail.html b/public_html/link_send_fail.html new file mode 100644 index 00000000..7f817a2c --- /dev/null +++ b/public_html/link_send_fail.html @@ -0,0 +1,45 @@ + + + + + Invalid Link + + + + +
+

No link sent. User not found or email already verified

+
+ + diff --git a/public_html/link_send_success.html b/public_html/link_send_success.html new file mode 100644 index 00000000..55d9cad6 --- /dev/null +++ b/public_html/link_send_success.html @@ -0,0 +1,45 @@ + + + + + Invalid Link + + + + +
+

Link Sent! Check your email.

+
+ + diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 7e27239c..fad7f4ef 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -6,7 +6,7 @@ const Config = require('../src/Config'); describe("Email Verification Token Expiration: ", () => { - it('show the invalid link page, if the user clicks on the verify email link after the email verify token expires', done => { + it('show the invalid verification link page, if the user clicks on the verify email link after the email verify token expires', done => { var user = new Parse.User(); var sendEmailOptions; var emailAdapter = { @@ -37,7 +37,7 @@ describe("Email Verification Token Expiration: ", () => { followRedirect: false, }, (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test'); done(); }); }, 1000); @@ -313,7 +313,7 @@ describe("Email Verification Token Expiration: ", () => { }); }); - it('clicking on the email verify link by an email VERIFIED user that was setup before enabling the expire email verify token should show an invalid link', done => { + it('clicking on the email verify link by an email VERIFIED user that was setup before enabling the expire email verify token should show email verify email success', done => { var user = new Parse.User(); var sendEmailOptions; var emailAdapter = { @@ -359,7 +359,7 @@ describe("Email Verification Token Expiration: ", () => { followRedirect: false, }, (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity'); done(); }); }) @@ -369,7 +369,7 @@ describe("Email Verification Token Expiration: ", () => { }); }); - it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show an invalid link', done => { + it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show invalid verficiation link page', done => { var user = new Parse.User(); var sendEmailOptions; var emailAdapter = { @@ -409,7 +409,7 @@ describe("Email Verification Token Expiration: ", () => { followRedirect: false, }, (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test'); done(); }); }) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index e127fd03..d1d1007b 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -655,7 +655,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }); }); - it('redirects you to invalid link if you try to validate a nonexistant users email', done => { + it('redirects you to invalid verification link page if you try to validate a nonexistant users email', done => { reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, @@ -671,7 +671,32 @@ describe("Custom Pages, Email Verification, Password Reset", () => { followRedirect: false, }, (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test'); + done(); + }); + }); + }); + + it('redirects you to link send fail page if you try to resend a link for a nonexistant user', done => { + reconfigureServer({ + appName: 'emailing app', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + }, + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + request.post('http://localhost:8378/1/apps/test/resend_verification_email', { + followRedirect: false, + form: { + username: "sadfasga" + } + }, (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html'); done(); }); }); @@ -685,7 +710,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => { followRedirect: false, }, (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test'); user.fetch() .then(() => { expect(user.get('emailVerified')).toEqual(false); diff --git a/src/Config.js b/src/Config.js index 8958a079..52d1bb81 100644 --- a/src/Config.js +++ b/src/Config.js @@ -234,6 +234,18 @@ export class Config { return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; } + get invalidVerificationLinkURL() { + return this.customPages.invalidVerificationLink || `${this.publicServerURL}/apps/invalid_verification_link.html`; + } + + get linkSendSuccessURL() { + return this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` + } + + get linkSendFailURL() { + return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html` + } + get verifyEmailSuccessURL() { return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`; } diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index b6ca02a5..305fdccf 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -60,11 +60,17 @@ export class UserController extends AdaptableController { updateFields._email_verify_token_expires_at = {__op: 'Delete'}; } - return this.config.database.update('_User', query, updateFields).then((document) => { - if (!document) { - throw undefined; + var checkIfAlreadyVerified = new RestQuery(this.config, Auth.master(this.config), '_User', {username: username, emailVerified: true}); + return checkIfAlreadyVerified.execute().then(result => { + if (result.results.length) { + return Promise.resolve(result.results.length[0]); } - return Promise.resolve(document); + return this.config.database.update('_User', query, updateFields).then((document) => { + if (!document) { + throw undefined + } + return Promise.resolve(document); + }) }); } @@ -134,6 +140,18 @@ export class UserController extends AdaptableController { }); } + resendVerificationEmail(username) { + return this.getUserIfNeeded({username: username}).then((aUser) => { + if (!aUser || aUser.emailVerified) { + throw undefined; + } + this.setEmailVerifyToken(aUser); + return this.config.database.update('_User', {username}, aUser).then(() => { + this.sendVerificationEmail(aUser); + }); + }); + } + setPasswordResetToken(email) { const token = { _perishable_token: randomString(25) }; diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index e3a569ff..012c13b3 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -31,7 +31,35 @@ export class PublicAPIRouter extends PromiseRouter { location: `${config.verifyEmailSuccessURL}?${params}` }); }, ()=> { + return this.invalidVerificationLink(req); + }) + } + + resendVerificationEmail(req) { + const username = req.body.username; + const appId = req.params.appId; + const config = new Config(appId); + + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + + if (!username) { return this.invalidLink(req); + } + + const userController = config.userController; + + return userController.resendVerificationEmail(username).then(() => { + return Promise.resolve({ + status: 302, + location: `${config.linkSendSuccessURL}` + }); + }, ()=> { + return Promise.resolve({ + status: 302, + location: `${config.linkSendFailURL}` + }); }) } @@ -123,6 +151,19 @@ export class PublicAPIRouter extends PromiseRouter { }); } + invalidVerificationLink(req) { + const config = req.config; + if (req.query.username && req.params.appId) { + const params = qs.stringify({username: req.query.username, appId: req.params.appId}); + return Promise.resolve({ + status: 302, + location: `${config.invalidVerificationLinkURL}?${params}` + }); + } else { + return this.invalidLink(req); + } + } + missingPublicServerURL() { return Promise.resolve({ text: 'Not found.', @@ -140,6 +181,10 @@ export class PublicAPIRouter extends PromiseRouter { req => { this.setConfig(req) }, req => { return this.verifyEmail(req); }); + this.route('POST', '/apps/:appId/resend_verification_email', + req => { this.setConfig(req); }, + req => { return this.resendVerificationEmail(req); }); + this.route('GET','/apps/choose_password', req => { return this.changePassword(req); });