Skip to content
This repository has been archived by the owner on May 7, 2021. It is now read-only.

Password Reset #355

Merged
merged 6 commits into from
Mar 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions src-backbone/app/js/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
let App = require('utils/sanaAppInstance');
let Helpers = require('utils/helpers');
let AuthLayoutView = require('views/auth/authLayoutView');
let SignupView = require('views/auth/signupView');
let LoginView = require('views/auth/loginView');
let SettingsView = require('views/auth/settingsView');
const App = require('utils/sanaAppInstance');
const Helpers = require('utils/helpers');
const AuthLayoutView = require('views/auth/authLayoutView');
const SignupView = require('views/auth/signupView');
const LoginView = require('views/auth/loginView');
const SettingsView = require('views/auth/settingsView');
const ResetPasswordView = require('views/auth/resetPasswordView');
const ResetPasswordCompleteView = require('views/auth/resetPasswordCompleteView');


module.exports = Marionette.Controller.extend({
Expand All @@ -20,6 +22,26 @@ module.exports = Marionette.Controller.extend({
authLayoutView.showChildView('authFormArea', new SettingsView());
},

routeResetPassword: function() {
if (App().session.isValid()) {
Helpers.navigateToDefaultLoggedIn();
return;
}
Helpers.arrivedOnView('Reset Password');

let authLayoutView = new AuthLayoutView();
App().RootView.switchMainView(authLayoutView);
authLayoutView.showChildView('authFormArea', new ResetPasswordView());
},

routeResetPasswordComplete: function(token) {
Helpers.arrivedOnView('Reset Password');

let authLayoutView = new AuthLayoutView();
App().RootView.switchMainView(authLayoutView);
authLayoutView.showChildView('authFormArea', new ResetPasswordCompleteView({token: token}));
},

routeSignup: function () {
if (App().session.isValid()) {
Helpers.navigateToDefaultLoggedIn();
Expand Down
12 changes: 7 additions & 5 deletions src-backbone/app/js/routers/authRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ module.exports = Marionette.AppRouter.extend({
},

appRoutes: {
'' : 'routeIndex',
'login' : 'routeLogin',
'logout' : 'routeLogout',
'signup' : 'routeSignup',
'account' : 'routeSettings',
'' : 'routeIndex',
'login' : 'routeLogin',
'logout' : 'routeLogout',
'signup' : 'routeSignup',
'account' : 'routeSettings',
'resetpassword' : 'routeResetPassword',
'resetpassword/:token' : 'routeResetPasswordComplete'
}

});
15 changes: 15 additions & 0 deletions src-backbone/app/js/templates/auth/resetPasswordCompleteView.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<form>
<div class="form-group">
<label for="new_password" class="sr-only">{{ i18n "New Password" }}</label>
<input type="password" name="new_password" class="form-control" id="new_password" placeholder="New Password">
</div>

<div class="form-group">
<label for="password_confirmation" class="sr-only">{{ i18n "Confirm Password" }}</label>
<input type="password" name="password_confirmation" class="form-control" id="password_confirmation" placeholder="Confirm Password">
</div>

<div>
<button type="submit" class="btn btn-primary btn-block" id="submit-btn">{{ i18n "Change Password" }}</button>
</div>
</form>
10 changes: 10 additions & 0 deletions src-backbone/app/js/templates/auth/resetPasswordView.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<form>
<div class="form-group">
<label for="email" class="sr-only">{{ i18n "Email" }}</label>
<input type="text" name="email" class="form-control" id="email" placeholder="Email">
</div>

<div>
<button type="submit" class="btn btn-primary btn-block" id="submit-btn">{{ i18n "Request Password Reset" }}</button>
</div>
</form>
2 changes: 2 additions & 0 deletions src-backbone/app/js/utils/configTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ module.exports = {
'terms',
'privacy',
'credits',
'resetpassword',
'resetpasswordcomplete',
],

ELEMENT_TYPES: [
Expand Down
69 changes: 69 additions & 0 deletions src-backbone/app/js/views/auth/resetPasswordCompleteView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const App = require('utils/sanaAppInstance');
const Helpers = require('utils/helpers');

module.exports = Marionette.ItemView.extend({

initialize: function(reset_token) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we usually put initialize after the other definitions (in this case, template, ui, and events)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no real convention but initialize at the beginning makes more sense. It is also how objects are defined on Backbone's site.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k 👍

this.token = reset_token.token;
},

template: require('templates/auth/resetPasswordCompleteView'),

ui: {
form: 'form',
},

events: {
'submit @ui.form': 'onSubmit',
},

onSubmit: function(event) {
event.preventDefault();

let self = this;
let $form = this.ui.form;

self.completeReset($form.serializeArray());
},

completeReset: function(data) {
let json = {};
data.forEach(function(item) {
if (item.value !== "") {
json[item.name] = item.value;
}
});
json.reset_token = this.token;

$.ajax({
type: 'POST',
url: '/api/passwords/reset_password_complete',
data: JSON.stringify(json),
beforeSend: function() {
App().RootView.showSpinner();
},
complete: function() {
App().RootView.hideSpinner();
},
success: function(response) {
Helpers.navigateToDefaultLoggedOut();
App().RootView.clearNotifications();
App().RootView.showNotification({
title: 'Success!',
desc: 'Password successfully reset!',
alertType: 'success',
});
},
error: function(errors) {
App().RootView.clearNotifications();
Object.keys(errors.responseJSON).forEach(function(key) {
App().RootView.showNotification({
title: 'There was a problem',
desc: errors.responseJSON[key]
});
});
}
});
},

});
63 changes: 63 additions & 0 deletions src-backbone/app/js/views/auth/resetPasswordView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
let App = require('utils/sanaAppInstance');
const Helpers = require('utils/helpers');

module.exports = Marionette.ItemView.extend({
template: require('templates/auth/resetPasswordView'),

ui: {
form: 'form',
},

events: {
'submit @ui.form': 'onSubmit',
},

onSubmit: function(event) {
event.preventDefault();

let self = this;
let $form = this.ui.form;

self.resetPassword($form.serializeArray());
},

resetPassword: function(data) {
let json = {};
data.forEach(function(item) {
if (item.value !== "") {
json[item.name] = item.value;
}
});

$.ajax({
type: 'POST',
data: JSON.stringify(json),
url: '/api/passwords/reset_password',
beforeSend: function() {
App().RootView.showSpinner();
},
complete: function() {
App().RootView.hideSpinner();
},
success: function(response) {
Helpers.navigateToDefaultLoggedOut();
App().RootView.clearNotifications();
App().RootView.showNotification({
title: 'Success!',
desc: 'Email to reset password sent!',
alertType: 'success',
});
},
error: function(errors) {
App().RootView.clearNotifications();
Object.keys(errors.responseJSON).forEach(function(key) {
App().RootView.showNotification({
title: 'There was a problem',
desc: errors.responseJSON[key]
});
});
}
});
},

});
6 changes: 6 additions & 0 deletions src-backbone/app/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
// Account Settings
"Successfuly updated account details!": "Successfuly updated account details!",
"There was a problem": "There was a problem",
"Password successfully reset!": "Password successfully reset!",
"Email to reset password sent!": "Email to reset password sent!",
"Confirm Password": "Confirm Password",
"New Password": "New Password",
"Change Password": "Change Password",
"Request Password Reset": "Request Password Reset",

// Procedures
"Hello username": "Hello {{ username }}",
Expand Down
1 change: 1 addition & 0 deletions src-backbone/gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ gulp.task('js-unit-test', ['js-lint', 'js-config'], function() {
gulp.src(Config.javascripts)
.pipe(filter('**/**_test.js'))
.pipe(mocha({
timeout: 10000,
require: [
'./test/unit/setup/globals',
],
Expand Down
20 changes: 20 additions & 0 deletions src-backbone/test/unit/auth/authController_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ describe('Auth Controller', function() {
let settingsView;
let settingsViewStub;

let resetPasswordView;
let resetPasswordViewStub;

let resetPasswordCompleteView;
let resetPasswordCompleteViewStub;

beforeEach(function() {
// Setup helpers
let helpers = require('utils/helpers');
Expand Down Expand Up @@ -72,6 +78,18 @@ describe('Auth Controller', function() {
};
settingsViewStub = sinon.stub().returns(settingsView);

// Setup resetPasswordView
resetPasswordView = {
name: 'resetPasswordView',
};
resetPasswordViewStub = sinon.stub().returns(resetPasswordView);

// Setup resetPasswordCompleteView
resetPasswordCompleteView = {
name: 'resetPasswordCompleteView',
};
resetPasswordCompleteViewStub = sinon.stub().returns(resetPasswordCompleteView);

// Setup authController
let AuthController = proxyquire('controllers/authController', {
'utils/sanaAppInstance': getAppInstance,
Expand All @@ -80,6 +98,8 @@ describe('Auth Controller', function() {
'views/auth/signupView': signupViewStub,
'views/auth/loginView': loginViewStub,
'views/auth/settingsView': settingsViewStub,
'views/auth/resetPasswordView': resetPasswordViewStub,
'views/auth/resetPasswordCompleteView': resetPasswordCompleteViewStub,
});
authController = new AuthController();
});
Expand Down
9 changes: 9 additions & 0 deletions src-django/api/templates/password_reset_template
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Hello,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this file have an extension?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean we can always make it a .txt but it doesn't make a difference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k 👍


You requested your password to be reset. Please follow the link provided in order to complete the steps. You have 48 hours to reset your password.

{{ link }}

If you didn't request the password reset, just ignore this message.

Thanks
53 changes: 51 additions & 2 deletions src-django/api/tests/test_users.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
from django.test import TestCase, Client
from django.test import TestCase, Client, override_settings
from django.contrib.auth.models import User
from django.core import mail
from django.core.cache import cache
from rest_framework import status
from rest_framework.authtoken.models import Token
from nose.tools import assert_equals, assert_not_equals, assert_true, assert_false
from nose.tools import assert_equals, assert_not_equals, assert_true, assert_false, assert_is_none
from api.startup import grant_permissions
from utils.helpers import add_token_to_header
from utils import factories
from mock import patch
import json


@override_settings(
CACHES={
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
},
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
CELERY_ALWAYS_EAGER=True,
BROKER_BACKEND='memory'
)
class UserTest(TestCase):

def setUp(self):
Expand All @@ -21,6 +34,8 @@ def setUp(self):
self.current_password = 'testpassword'
self.user.set_password(self.current_password)
self.user.save()
self.reset_password_url = '/api/passwords/reset_password'
self.reset_password_complete_url = '/api/passwords/reset_password_complete'
grant_permissions()

def test_update_email(self):
Expand Down Expand Up @@ -77,3 +92,37 @@ def test_update_missing_field(self):

assert_equals(response.status_code, status.HTTP_400_BAD_REQUEST)
assert_equals(User.objects.get(pk=self.user.id), self.user)

@patch('api.views.PasswordResetTokenGenerator.make_token')
def test_reset_password(self, PasswordResetTokenGeneratorMock):
data = {
'email': self.user.email,
}

PasswordResetTokenGeneratorMock.return_value = 'abc'

response = self.client.post(
path=self.reset_password_url,
data=json.dumps(data),
content_type='application/json',
)

assert_equals(response.status_code, status.HTTP_201_CREATED)
assert_equals(len(mail.outbox), 1)
assert_equals(mail.outbox[0].subject, 'Reset Your Password')

data = {
'reset_token': 'abc',
'new_password': self.new_password,
'password_confirmation': self.new_password,
}

response = self.client.post(
path=self.reset_password_complete_url,
data=json.dumps(data),
content_type='application/json'
)

assert_equals(response.status_code, status.HTTP_200_OK)
assert_true(User.objects.get(pk=self.user.id).check_password(self.new_password))
assert_is_none(cache.get(self.user.email))
1 change: 1 addition & 0 deletions src-django/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
router.register(r'concepts', views.ConceptViewSet, base_name='concept')
router.register(r'conditionals', views.ShowIfViewSet, base_name='conditional')
router.register(r'users', views.UserViewSet, base_name='user')
router.register(r'passwords', views.UserPasswordViewSet, base_name='password')

urlpatterns = [
url(r'^', include(router.urls))
Expand Down
Loading