From 41eff2c4db9ce66a8f0b6c3a99293c336e40e3cb Mon Sep 17 00:00:00 2001 From: Jacob Gable Date: Thu, 21 Nov 2013 21:17:38 -0600 Subject: [PATCH] Improved Password Reset Tool Closes #1471 - add api and User model methods for generating and validating tokens - add routes and handlers for reset password pages - add client styles and views for reset password form - some basic integration tests for User model methods --- assets/sass/layouts/auth.scss | 10 ++-- router.js | 7 ++- tpl/reset.hbs | 9 +++ views/login.js | 109 ++++++++++++++++++++++++++++++---- 4 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 tpl/reset.hbs diff --git a/assets/sass/layouts/auth.scss b/assets/sass/layouts/auth.scss index 867df8257..44c9ea300 100644 --- a/assets/sass/layouts/auth.scss +++ b/assets/sass/layouts/auth.scss @@ -15,7 +15,8 @@ .ghost-login, .ghost-signup, -.ghost-forgotten { +.ghost-forgotten, +.ghost-reset { color: $midgrey; background: $darkgrey; @@ -35,7 +36,8 @@ .login-box, .signup-box, -.forgotten-box { +.forgotten-box, +.reset-box { max-width: 530px; height: 90%; margin: 0 auto; @@ -177,10 +179,10 @@ /* ============================================================================= - 2. Signup + 2. Signup and Reset ============================================================================= */ -#signup { +#signup, #reset { @include box-sizing(border-box); max-width: 280px; color: lighten($midgrey, 15%); diff --git a/router.js b/router.js index bd69855f5..e0cadb1d0 100644 --- a/router.js +++ b/router.js @@ -13,7 +13,8 @@ 'register/' : 'register', 'signup/' : 'signup', 'signin/' : 'login', - 'forgotten/' : 'forgotten' + 'forgotten/' : 'forgotten', + 'reset/:token/' : 'reset' }, signup: function () { @@ -28,6 +29,10 @@ Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-forgotten-box' }); }, + reset: function (token) { + Ghost.currentView = new Ghost.Views.ResetPassword({ el: '.js-reset-box', token: token }); + }, + blog: function () { var posts = new Ghost.Collections.Posts(); NProgress.start(); diff --git a/tpl/reset.hbs b/tpl/reset.hbs new file mode 100644 index 000000000..1204617c2 --- /dev/null +++ b/tpl/reset.hbs @@ -0,0 +1,9 @@ +
+
+ +
+
+ +
+ +
diff --git a/views/login.js b/views/login.js index da9b24d9b..7da7b78a3 100644 --- a/views/login.js +++ b/views/login.js @@ -6,9 +6,6 @@ initialize: function () { this.render(); - $(".js-login-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () { - $("[name='email']").focus(); - }); }, templateName: "login", @@ -17,6 +14,13 @@ 'submit #login': 'submitHandler' }, + afterRender: function () { + var self = this; + this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () { + self.$("[name='email']").focus(); + }); + }, + submitHandler: function (event) { event.preventDefault(); var email = this.$el.find('.email').val(), @@ -61,9 +65,6 @@ initialize: function () { this.render(); - $(".js-signup-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () { - $("[name='name']").focus(); - }); }, templateName: "signup", @@ -72,11 +73,21 @@ 'submit #signup': 'submitHandler' }, + afterRender: function () { + var self = this; + + this.$el + .css({"opacity": 0}) + .animate({"opacity": 1}, 500, function () { + self.$("[name='name']").focus(); + }); + }, + submitHandler: function (event) { event.preventDefault(); - var name = this.$el.find('.name').val(), - email = this.$el.find('.email').val(), - password = this.$el.find('.password').val(); + var name = this.$('.name').val(), + email = this.$('.email').val(), + password = this.$('.password').val(); // This is needed due to how error handling is done. If this is not here, there will not be a time // when there is no error. @@ -119,9 +130,6 @@ initialize: function () { this.render(); - $(".js-forgotten-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () { - $("[name='email']").focus(); - }); }, templateName: "forgotten", @@ -130,6 +138,13 @@ 'submit #forgotten': 'submitHandler' }, + afterRender: function () { + var self = this; + this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () { + self.$("[name='email']").focus(); + }); + }, + submitHandler: function (event) { event.preventDefault(); @@ -166,4 +181,74 @@ } } }); + + Ghost.Views.ResetPassword = Ghost.View.extend({ + templateName: 'reset', + + events: { + 'submit #reset': 'submitHandler' + }, + + initialize: function (attrs) { + attrs = attrs || {}; + + this.token = attrs.token; + + this.render(); + }, + + afterRender: function () { + var self = this; + this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () { + self.$("[name='newpassword']").focus(); + }); + }, + + submitHandler: function (ev) { + ev.preventDefault(); + + var self = this, + newPassword = this.$('input[name="newpassword"]').val(), + ne2Password = this.$('input[name="ne2password"]').val(); + + if (newPassword !== ne2Password) { + Ghost.notifications.addItem({ + type: 'error', + message: "Your passwords do not match.", + status: 'passive' + }); + + return; + } + + this.$('input, button').prop('disabled', true); + + $.ajax({ + url: '/ghost/reset/' + this.token + '/', + type: 'POST', + headers: { + 'X-CSRF-Token': $("meta[name='csrf-param']").attr('content') + }, + data: { + newpassword: newPassword, + ne2password: ne2Password + }, + success: function (msg) { + window.location.href = msg.redirect; + }, + error: function (xhr) { + self.$('input, button').prop('disabled', false); + + Ghost.notifications.clearEverything(); + Ghost.notifications.addItem({ + type: 'error', + message: Ghost.Views.Utils.getRequestErrorMessage(xhr), + status: 'passive' + }); + } + }); + + return false; + } + }); }());