From 4c6f866ba2c3b897d0519ee32859e787a85a23a6 Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:09:18 -0400 Subject: [PATCH] Add resend verification email page --- src/js/apps/discovery/router.js | 2 +- src/js/components/api_targets.js | 1 + src/js/components/session.js | 53 ++++++++++ .../authentication/templates/log-in.html | 5 +- .../authentication/templates/register.html | 2 +- .../templates/resend-verification-email.html | 25 +++++ src/js/widgets/authentication/widget.js | 100 ++++++++++++++++-- test/mocha/js/components/session.spec.js | 1 + 8 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 src/js/widgets/authentication/templates/resend-verification-email.html diff --git a/src/js/apps/discovery/router.js b/src/js/apps/discovery/router.js index 809255340..ffaa8008a 100644 --- a/src/js/apps/discovery/router.js +++ b/src/js/apps/discovery/router.js @@ -214,7 +214,7 @@ define([ if ( subView && !_.contains( - ['login', 'register', 'reset-password-1', 'reset-password-2'], + ['login', 'register', 'reset-password-1', 'reset-password-2', 'resend-verification-email'], subView ) ) { diff --git a/src/js/components/api_targets.js b/src/js/components/api_targets.js index 0f66e3a40..212ba8ff5 100644 --- a/src/js/components/api_targets.js +++ b/src/js/components/api_targets.js @@ -34,6 +34,7 @@ define([], function () { LOGIN: 'accounts/user/login', LOGOUT: 'accounts/user/logout', VERIFY: 'accounts/verify', + RESEND_VERIFY: `accounts/user/{email}/verify`, RESET_PASSWORD: 'accounts/user/reset-password', CHANGE_PASSWORD: 'accounts/user/change-password', CHANGE_EMAIL: 'accounts/user/change-email', diff --git a/src/js/components/session.js b/src/js/components/session.js index 5f5ea5997..f843a6a9b 100644 --- a/src/js/components/session.js +++ b/src/js/components/session.js @@ -208,6 +208,58 @@ define([ }); }, + + /** + * Resend verification email + * @param {string} email + */ + resendVerificationEmail: function(email) { + const self = this; + this.sendRequestWithNewCSRF(function(csrfToken) { + const request = new ApiRequest({ + target: ApiTargets.RESEND_VERIFY.replace('{email}', email), + query: new ApiQuery({}), + options: { + type: 'PUT', + headers: { 'X-CSRFToken': csrfToken }, + done: function() { + const pubsub = self.getPubSub(); + pubsub.publish( + pubsub.USER_ANNOUNCEMENT, + 'resend_verification_email_success' + ); + }, + fail: function(xhr) { + const pubsub = self.getPubSub(); + const error = utils.extractErrorMessageFromAjax( + xhr, + 'error unknown' + ); + const message = `Resending verification email was unsuccessful (${error})`; + pubsub.publish( + pubsub.ALERT, + new ApiFeedback({ + code: 0, + msg: message, + type: 'danger', + fade: true, + }) + ); + pubsub.publish( + pubsub.USER_ANNOUNCEMENT, + 'resend_verification_email_fail', + message + ); + }, + }, + }); + + return this.getBeeHive() + .getService('Api') + .request(request); + }); + }, + setChangeToken: function(token) { this.model.set('resetPasswordToken', token); }, @@ -365,6 +417,7 @@ define([ resetPassword1: 'sends an email to account', resetPassword2: 'updates the password', setChangeToken: 'the router stores the token to reset password here', + resendVerificationEmail: 'resends the verification email', }, }); diff --git a/src/js/widgets/authentication/templates/log-in.html b/src/js/widgets/authentication/templates/log-in.html index a81a170f1..0d90c0c78 100644 --- a/src/js/widgets/authentication/templates/log-in.html +++ b/src/js/widgets/authentication/templates/log-in.html @@ -28,7 +28,10 @@
- Don't have an account yet? Register + Don't have an account yet?  +
+
+
diff --git a/src/js/widgets/authentication/templates/register.html b/src/js/widgets/authentication/templates/register.html index 0c9850356..56f0ad794 100644 --- a/src/js/widgets/authentication/templates/register.html +++ b/src/js/widgets/authentication/templates/register.html @@ -55,7 +55,7 @@ Already have an account? Login -
+
This site is protected by reCAPTCHA and the Google Privacy Policy and diff --git a/src/js/widgets/authentication/templates/resend-verification-email.html b/src/js/widgets/authentication/templates/resend-verification-email.html new file mode 100644 index 000000000..d6c989802 --- /dev/null +++ b/src/js/widgets/authentication/templates/resend-verification-email.html @@ -0,0 +1,25 @@ +
+
Resend Verification Email
+
+
+ + + +
+ +
+
+ +
+ + {{#if hasError}} +
+
+
+

{{errorMsg}}

+

Dismiss

+
+
+
+ {{/if}} +
diff --git a/src/js/widgets/authentication/widget.js b/src/js/widgets/authentication/widget.js index 66c089963..757817cc0 100644 --- a/src/js/widgets/authentication/widget.js +++ b/src/js/widgets/authentication/widget.js @@ -10,6 +10,7 @@ define([ 'hbs!js/widgets/authentication/templates/container', 'hbs!js/widgets/authentication/templates/reset-password-1', 'hbs!js/widgets/authentication/templates/reset-password-2', + 'hbs!js/widgets/authentication/templates/resend-verification-email', 'js/components/user', 'analytics', 'backbone-validation', @@ -26,8 +27,9 @@ define([ ContainerTemplate, ResetPassword1Template, ResetPassword2Template, + ResendVerificationEmail, User, - analytics + analytics, ) { // Creating module level variable since I can't figure out best way to pass this value into a subview from the model // This value should be always available, and unchanging, so should be safe to set like this here @@ -54,11 +56,11 @@ define([ if (typeof formName === 'string') { window.grecaptcha.ready(() => window.grecaptcha - .execute(siteKey, { action: `auth/${formName}` }) - .then((token) => { - this.model.set('g-recaptcha-response', token); - FormFunctions.triggerSubmit.apply(this, arguments); - }) + .execute(siteKey, { action: `auth/${formName}` }) + .then((token) => { + this.model.set('g-recaptcha-response', token); + FormFunctions.triggerSubmit.apply(this, arguments); + }), ); } else { FormFunctions.triggerSubmit.apply(this, arguments); @@ -301,6 +303,39 @@ define([ }, }); + const ResendVerificationModel = FormModel.extend({ + skipReset: ['email'], + validation: { + email: { + required: true, + pattern: 'email', + msg: '(A valid email is required)', + }, + }, + + target: 'RESEND_VERIFY', + method: 'PUT', + }); + + const ResendVerificationView = FormView.extend({ + template: ResendVerificationEmail, + + className: 'resend-verification', + + bindings: { + 'input[name=email]': { + observe: 'email', + setOptions: { + validate: true, + }, + }, + }, + + onRender: function() { + this.activateValidation(); + }, + }); + var StateModel = Backbone.Model.extend({ defaults: function() { return { @@ -317,6 +352,7 @@ define([ this.registerModel = new RegisterModel(); this.resetPassword1Model = new ResetPassword1Model(); this.resetPassword2Model = new ResetPassword2Model(); + this.resendVerificationModel = new ResendVerificationModel(); }, modelEvents: { 'change:subView': 'renderSubView' }, @@ -331,6 +367,8 @@ define([ 'click .show-login': 'navigateToLoginForm', 'click .show-register': 'navigateToRegisterForm', 'click .show-reset-password-1': 'navigateToResetPassword1Form', + 'click .show-resend-verification-email': + 'navigateToResendVerificationForm', }, onRender: function() { @@ -348,6 +386,8 @@ define([ this.showResetPasswordForm1(); } else if (subView === 'reset-password-2') { this.showResetPasswordForm2(); + } else if (subView === 'resend-verification-email') { + this.showResendVerificationForm(); } }, @@ -399,11 +439,28 @@ define([ this.container.show(view); }, + showResendVerificationForm: function(error) { + const view = new ResendVerificationView({ model: this.resendVerificationModel }); + + // show error message + if (error) { + view.model.set({ hasError: true, errorMsg: error }); + } + + view.on('submit-form', this.forwardSubmit, this); + this.container.show(view); + }, + showRegisterSuccessView: function() { var view = new SuccessView({ title: 'Registration Successful' }); this.container.show(view); }, + showResendVerificationSuccessView: function() { + var view = new SuccessView({ title: 'Verification Email Sent Successfully' }); + this.container.show(view); + }, + showResetPasswordSuccessView: function() { var view = new SuccessView({ title: 'Password Reset Successful' }); this.container.show(view); @@ -428,12 +485,17 @@ define([ this.listenTo( this.view, 'navigateToRegisterForm', - this.navigateToRegisterForm + this.navigateToRegisterForm, ); this.listenTo( this.view, 'navigateToResetPassword1Form', - this.navigateToResetPassword1Form + this.navigateToResetPassword1Form, + ); + this.listenTo( + this.view, + 'navigateToResendVerificationForm', + this.navigateToResendVerificationForm ); this.nextNav = null; @@ -459,6 +521,10 @@ define([ this._navigate({ subView: 'reset-password-1' }); }, + navigateToResendVerificationForm: function() { + this._navigate({ subView: 'resend-verification-email' }); + }, + _navigate: function(opts) { var pubsub = this.getPubSub(); pubsub.publish(pubsub.NAVIGATE, 'authentication-page', opts); @@ -523,6 +589,17 @@ define([ auth_error: msg, }); break; + case 'resend_verification_email_success': + this.view.showResendVerificationSuccessView(); + this.fireAnalytics('resend-verification', { + auth_result: 'resend_verification_email_success', + }); + break; + case 'resend_verification_email_fail': + this.fireAnalytics('resend-verification', { + auth_result: 'resend_verification_email_fail', + }); + break; } }, @@ -547,11 +624,14 @@ define([ } }); break; - case 'RESET_PASSWORD': { + case 'RESET_PASSWORD': model.method === 'POST' ? session.resetPassword1(model.toJSON()) : session.resetPassword2(model.toJSON()); - } + break; + case 'RESEND_VERIFY': + session.resendVerificationEmail(model.get('email')); + break; } }, diff --git a/test/mocha/js/components/session.spec.js b/test/mocha/js/components/session.spec.js index 157980f52..15ea51508 100644 --- a/test/mocha/js/components/session.spec.js +++ b/test/mocha/js/components/session.spec.js @@ -21,6 +21,7 @@ define([ 'resetPassword1', 'resetPassword2', 'setChangeToken', + 'resendVerificationEmail', '__facade__', 'mixIn', ]);