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

Commit

Permalink
Merge pull request #355 from SanaMobile/fasih/password-reset
Browse files Browse the repository at this point in the history
Password Reset
  • Loading branch information
Ommy committed Mar 2, 2016
2 parents 294e7b5 + 6f1f53b commit 18bcf14
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 14 deletions.
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) {
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,

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

0 comments on commit 18bcf14

Please sign in to comment.