Merge branch 'issue-121-password-reset-screens' of github.com:mozilla/fxa-content-server into shane-pwreset

This commit is contained in:
Zachary Carter 2014-01-14 15:46:50 -08:00
Родитель acbc85a08d fe36cf63ad
Коммит 45963b8e6d
25 изменённых файлов: 730 добавлений и 44 удалений

Просмотреть файл

@ -13,6 +13,25 @@ define([
'processed/constants'
],
function (FxaClient, Constants) {
// placeholder promise to stand in for FxaClient functionality that is
// not yet ready.
function PromiseMock() {
}
PromiseMock.prototype = {
then: function (callback) {
var promise = new PromiseMock();
if (callback) {
callback();
}
return promise;
},
done: function (callback) {
if (callback) {
callback();
}
}
};
function FxaClientWrapper() {
this.client = new FxaClient(Constants.FXA_ACCOUNT_SERVER);
}
@ -33,6 +52,14 @@ function (FxaClient, Constants) {
verifyCode: function (uid, code) {
return this.client.verifyCode(uid, code);
},
requestPasswordReset: function () {
return new PromiseMock();
},
completePasswordReset: function () {
return new PromiseMock();
}
};

29
app/scripts/lib/url.js Normal file
Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// utilities to deal with urls
'use strict';
define(['underscore'],
function (_) {
return {
searchParam: function (name, str) {
var search = (str || window.location.search).replace(/^\?/, '');
if (! search) {
return;
}
var pairs = search.split('&');
var terms = {};
_.each(pairs, function (pair) {
var keyValue = pair.split('=');
terms[keyValue[0]] = decodeURIComponent(keyValue[1]);
});
return terms[name];
}
};
});

42
app/scripts/lib/xss.js Normal file
Просмотреть файл

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Basic XSS protection
'use strict';
define([
'underscore',
'processed/constants'
],
function(_, Constants) {
return {
// only allow http or https URLs, encoding the URL.
href: function(text) {
if (! _.isString(text)) {
return;
}
if (! /^https?:\/\//.test(text)) {
return;
}
var encodedURI = encodeURI(text);
// All browsers have a max length of URI that they can handle.
// IE8 has the shortest total length at 2083 bytes and 2048 characters
// for GET requests.
// See http://support.microsoft.com/kb/q208427
// Check the total encoded URI length
if (encodedURI.length > Constants.URL_MAX_LENGTH) {
return;
}
return encodedURI;
}
};
});

Просмотреть файл

@ -6,7 +6,13 @@
define([], function () {
return {
FXA_ACCOUNT_SERVER: '/* @echo fxaccountUrl */'
FXA_ACCOUNT_SERVER: '/* @echo fxaccountUrl */',
// All browsers have a max length of URI that they can handle.
// IE8 has the shortest total length at 2083 bytes and 2048 characters
// for GET requests.
// See http://support.microsoft.com/kb/q208427
URL_MAX_LENGTH: 2048
};
});

Просмотреть файл

@ -19,9 +19,13 @@ define([
'views/create_account',
'views/cannot_create_account',
'views/complete_sign_up',
'views/reset_password',
'views/confirm_reset_password',
'views/complete_reset_password',
'views/reset_password_complete',
'transit'
],
function ($, Backbone, IntroView, SignInView, SignUpView, ConfirmView, SettingsView, TosView, PpView, AgeView, BirthdayView, CreateAccountView, CannotCreateAccountView, CompleteSignUpView) {
function ($, Backbone, IntroView, SignInView, SignUpView, ConfirmView, SettingsView, TosView, PpView, AgeView, BirthdayView, CreateAccountView, CannotCreateAccountView, CompleteSignUpView, ResetPasswordView, ConfirmResetPasswordView, CompleteResetPasswordView, ResetPasswordCompleteView) {
var Router = Backbone.Router.extend({
routes: {
'': 'showIntro',
@ -35,7 +39,11 @@ function ($, Backbone, IntroView, SignInView, SignUpView, ConfirmView, SettingsV
'birthday': 'showBirthday',
'create_account': 'showCreateAccount',
'cannot_create_account': 'showCannotCreateAccount',
'verify_email': 'showCompleteSignUp'
'verify_email': 'showCompleteSignUp',
'reset_password': 'showResetPassword',
'confirm_reset_password': 'showConfirmResetPassword',
'complete_reset_password': 'showCompleteResetPassword',
'reset_password_complete': 'showResetPasswordComplete'
},
initialize: function () {
@ -95,6 +103,22 @@ function ($, Backbone, IntroView, SignInView, SignUpView, ConfirmView, SettingsV
this.showView(new CompleteSignUpView());
},
showResetPassword: function () {
this.showView(new ResetPasswordView());
},
showConfirmResetPassword: function () {
this.showView(new ConfirmResetPasswordView());
},
showCompleteResetPassword: function () {
this.showView(new CompleteResetPasswordView());
},
showResetPasswordComplete: function () {
this.showView(new ResetPasswordCompleteView());
},
showView: function (view) {
if (this.currentView) {
this.currentView.destroy();

Просмотреть файл

@ -0,0 +1,24 @@
<header>
<h1 id='fxa-complete-reset-password-header'>{{#t}}Firefox Accounts{{/t}}</h1>
<h2>{{#t}}Please create a new password{{/t}}</h2>
</header>
<section>
<div class="error"></div>
<form>
<div class="input-row">
<input type="password" class="password" id="password" placeholder="{{#t}}Password{{/t}}" pattern=".{8,}">
</div>
<div class="input-row">
<input type="password" class="password" id="vpassword" placeholder="{{#t}}Repeat Password{{/t}}" pattern=".{8,}">
</div>
<div class="button-row">
<button type="submit">{{#t}}Next &gt;{{/t}}</button>
</div>
</form>
</section>

Просмотреть файл

@ -0,0 +1,26 @@
<header>
<h1 id="fxa-confirm-reset-password-header">{{#t}}Firefox Accounts{{/t}}</h1>
<h2>{{#t}}Password Reset Email Sent{{/t}}</h2>
</header>
<section>
<div class="error"></div>
<div class="placeholder email-placeholder">
{{#t}}Email{{/t}}
</div>
<p>{{#t}}Your password reset link awates at:{{/t}}
<br/>
<strong>{{email}}.</strong>
</p>
<div class="links">
{{#t}}Email not arriving?{{/t}} <a>{{#t}}Send again{{/t}}</a>
</div>
</section>
<footer>
</footer>

Просмотреть файл

@ -0,0 +1,34 @@
<header>
<h1 id='fxa-reset-password-header'>{{#t}}Firefox Accounts{{/t}}</h1>
<h2>{{#t}}Reset Password{{/t}}</h2>
</header>
<section>
<div class="error"></div>
<form>
<label for="email">
{{#t}}Enter your email address, and we'll email you instructions on how to reset your password{{/t}}
</label>
<div class="input-row">
<input name="email" type="email" class="email" placeholder="{{#t}}Email{{/t}}">
</div>
<div class="button-row">
<button type="submit">{{#t}}Submit{{/t}}</button>
</div>
</form>
<div class="links">
<a href="/signin">{{#t}}Sign In{{/t}}</a>
<a href="/signup">{{#t}}Create Account{{/t}}</a>
</div>
</section>
<footer>
</footer>

Просмотреть файл

@ -0,0 +1,22 @@
<header>
<h1 id='fxa-reset-password-complete-header'>{{#t}}Firefox Accounts{{/t}}</h1>
<h2>{{#t}}Password Reset!{{/t}}</h2>
</header>
<section>
<div class="error"></div>
{{#redirectTo}}
<p>
{{#t}}Continue to{{/t}} {{ service }} {{#t}}on the <strong>Initial Client</strong>.{{/t}}
</p>
<a href="{{ redirectTo }}">Continue</a>
{{/redirectTo}}
</section>
<footer>
</footer>

Просмотреть файл

@ -22,10 +22,11 @@
</form>
<div class="links">
<a href="">{{#t}}Forgot Password{{/t}}</a> • <a href="/signup">{{#t}}Create Account{{/t}}</a>
<a href="/reset_password">{{#t}}Forgot Password{{/t}}</a>
<a href="/signup">{{#t}}Create Account{{/t}}</a>
</div>
</section>
<footer>
</footer>

Просмотреть файл

@ -86,6 +86,17 @@ function(_, Backbone) {
_.invoke(this.subviews, 'destroy');
this.subviews = [];
},
isElementValid: function (selector) {
var el = this.$(selector);
var value = el.val();
return value && el[0].validity.valid;
},
displayError: function(msg) {
// TODO - run the error message through the translator
this.$('.error').html(msg);
}
});

Просмотреть файл

@ -0,0 +1,89 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'underscore',
'views/base',
'stache!templates/complete_reset_password',
'lib/fxa-client',
'lib/session',
'lib/url'
],
function (_, BaseView, Template, FxaClient, Session, Url) {
var View = BaseView.extend({
template: Template,
className: 'complete_reset_password',
events: {
'submit form': 'submit'
},
afterRender: function () {
this.uid = Url.searchParam('uid');
if (! this.uid) {
return this.displayError('no uid specified');
}
this.code = Url.searchParam('code');
if (! this.code) {
return this.displayError('no code specified');
}
},
submit: function (event) {
event.preventDefault();
if (! (this.uid &&
this.code &&
this._validatePasswords())) {
return;
}
var password = this._getPassword();
var client = new FxaClient();
client.completePasswordReset(password, this.uid, this.code)
.done(_.bind(this._onResetCompleteSuccess, this),
_.bind(this._onResetCompleteFailure, this));
},
_onResetCompleteSuccess: function () {
// This information will be displayed on the
// reset_password_complete screen.
Session.service = Url.searchParam('service');
Session.redirectTo = Url.searchParam('redirectTo');
router.navigate('reset_password_complete', { trigger: true });
},
_onResetCompleteFailure: function (err) {
this.displayError(err.message);
},
_validatePasswords: function () {
if (! (this.isElementValid('#password') &&
this.isElementValid('#vpassword'))) {
return false;
}
if (this._getPassword() !== this._getVPassword()) {
this.displayError('passwords do not match');
return false;
}
return true;
},
_getPassword: function () {
return this.$('#password').val();
},
_getVPassword: function () {
return this.$('#vpassword').val();
}
});
return View;
});

Просмотреть файл

@ -9,26 +9,11 @@ define([
'views/base',
'stache!templates/complete_sign_up',
'lib/session',
'lib/fxa-client'
'lib/fxa-client',
'lib/url',
'lib/xss'
],
function (_, BaseView, CompleteSignUpTemplate, Session, FxaClient) {
function getSearchParam(name) {
var search = window.location.search.replace(/^\?/, '');
if (! search) {
return;
}
var pairs = search.split('&');
var terms = {};
_.each(pairs, function (pair) {
var keyValue = pair.split('=');
terms[keyValue[0]] = keyValue[1];
});
return terms[name];
}
function (_, BaseView, CompleteSignUpTemplate, Session, FxaClient, Url, Xss) {
var CompleteSignUpView = BaseView.extend({
template: CompleteSignUpTemplate,
className: 'complete_sign_up',
@ -36,34 +21,32 @@ function (_, BaseView, CompleteSignUpTemplate, Session, FxaClient) {
context: function () {
return {
email: Session.email,
siteName: getSearchParam('service'),
redirectTo: getSearchParam('service')
service: Url.searchParam('service'),
redirectTo: Xss.href(Url.searchParam('redirectTo'))
};
},
afterRender: function () {
var uid = getSearchParam('uid');
var uid = Url.searchParam('uid');
if (! uid) {
return this._displayError('no uid specified');
return this.displayError('no uid specified');
}
var code = getSearchParam('code');
var code = Url.searchParam('code');
if (! code) {
return this._displayError('no code specified');
return this.displayError('no code specified');
}
var client = new FxaClient();
client.verifyCode(uid, code)
.then(function () {
// TODO - we could go to a "sign_up_complete" screen here.
this.$('#fxa-complete-sign-up-success').show();
}.bind(this), function (err) {
this._displayError(err.message);
this.displayError(err.message);
}.bind(this));
},
_displayError: function (msg) {
this.$('.error').html(msg);
}
});
return CompleteSignUpView;

Просмотреть файл

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'views/base',
'stache!templates/confirm_reset_password',
'lib/session'
],
function (BaseView, Template, Session) {
var View = BaseView.extend({
template: Template,
className: 'confirm-reset-password',
context: function () {
return {
email: Session.email
};
}
});
return View;
});

Просмотреть файл

@ -0,0 +1,58 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'underscore',
'views/base',
'stache!templates/reset_password',
'lib/fxa-client',
'lib/session'
],
function (_, BaseView, Template, FxaClient, Session) {
var View = BaseView.extend({
template: Template,
className: 'reset_password',
events: {
'submit form': 'requestPasswordReset'
},
requestPasswordReset: function (event) {
event.preventDefault();
if (! this._validateEmail()) {
return;
}
var email = this._getEmail();
var client = new FxaClient();
client.requestPasswordReset(email)
.done(this._onRequestResetSuccess.bind(this),
this._onRequestResetFailure.bind(this));
},
_onRequestResetSuccess: function () {
Session.email = this._getEmail();
router.navigate('confirm_reset_password', { trigger: true });
},
_onRequestResetFailure: function (err) {
this.displayError(err.message);
},
_getEmail: function () {
return this.$('.email').val();
},
_validateEmail: function () {
return this.isElementValid('.email');
}
});
return View;
});

Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'underscore',
'views/base',
'stache!templates/reset_password_complete',
'lib/session',
'lib/xss'
],
function (_, BaseView, Template, Session, Xss) {
var View = BaseView.extend({
template: Template,
className: 'reset_password_complete',
context: function () {
return {
email: Session.email,
service: Session.service,
redirectTo: Xss.href(Session.redirectTo)
};
}
});
return View;
});

Просмотреть файл

@ -32,17 +32,11 @@ function (BaseView, SignUpTemplate, Session) {
},
_validateEmail: function () {
return this._isElementValid('.email');
return this.isElementValid('.email');
},
_validatePassword: function () {
return this._isElementValid('.password');
},
_isElementValid: function (selector) {
var el = this.$(selector);
var value = el.val();
return value && el[0].validity.valid;
return this.isElementValid('.password');
}
});

Просмотреть файл

@ -48,7 +48,9 @@ require.config({
require([
'mocha',
'../tests/setup',
'../tests/spec/lib/channels/fx-desktop'
'../tests/spec/lib/channels/fx-desktop',
'../tests/spec/lib/xss',
'../tests/spec/lib/url'
],
function (Mocha) {
var runner = Mocha.run();

37
app/tests/spec/lib/url.js Normal file
Просмотреть файл

@ -0,0 +1,37 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'mocha',
'chai',
'underscore',
'lib/url',
'processed/constants'
],
function (mocha, chai, _, Url, Constants) {
var assert = chai.assert;
var channel;
describe('lib/url', function () {
describe('searchParam', function () {
it('returns a parameter from window.location.serach, if it exists',
function() {
assert.equal(Url.searchParam('color', '?color=green'), 'green');
});
it('returns undefined if parameter does not exist', function() {
assert.isUndefined(Url.searchParam('animal', '?color=green'));
});
it('does not throw if str override is not specified', function() {
assert.isUndefined(Url.searchParam('animal'));
});
});
});
});

96
app/tests/spec/lib/xss.js Normal file
Просмотреть файл

@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
define([
'mocha',
'chai',
'underscore',
'lib/xss',
'processed/constants'
],
function (mocha, chai, _, XSS, Constants) {
var assert = chai.assert;
var channel;
describe('lib/xss', function () {
describe('href', function () {
function expectEmpty(url) {
assert.isUndefined(XSS.href(url));
}
it('allows http href', function () {
assert.equal(XSS.href('http://all.good'), 'http://all.good');
});
it('allows https href', function () {
assert.equal(XSS.href('https://all.good'), 'https://all.good');
});
it('allows href with query parameters', function () {
assert.equal(XSS.href('https://all.good?with_query'),
'https://all.good?with_query');
});
it('allows but escapes URLs that try to break out', function () {
assert.equal(XSS.href('http://href.gone.bad" onclick="javascript(1)"'),
'http://href.gone.bad%22%20onclick=%22javascript(1)%22');
});
it('disallows javascript: href', function () {
expectEmpty('javascript:alert(1)');
});
it('disallows href without a scheme', function () {
expectEmpty('no.scheme');
});
it('disallows relative scheme', function () {
expectEmpty('//relative.scheme');
});
it('disallows data URI scheme', function () {
expectEmpty('data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D');
});
it('only allows strings', function () {
var disallowedItems = [
1,
true,
new Date(),
{},
[]
];
_.each(disallowedItems, expectEmpty);
});
it('allows hrefs of the max length', function() {
var maxLength = Constants.URL_MAX_LENGTH;
var allowed = "http://";
while (allowed.length < maxLength) {
allowed += 'a';
}
assert.equal(XSS.href(allowed), allowed);
});
it('disallowed hrefs that are too long', function() {
var maxLength = Constants.URL_MAX_LENGTH;
var tooLong = "http://";
while (tooLong.length < maxLength + 1) {
tooLong += 'a';
}
expectEmpty(tooLong);
});
});
});
});

Просмотреть файл

@ -11,6 +11,10 @@ define([
'./functional/age',
'./functional/birthday',
'./functional/confirm',
'./functional/reset_password',
'./functional/confirm_reset_password',
'./functional/complete_reset_password',
'./functional/reset_password_complete',
'./functional/mocha'
], function () {
'use strict';

Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define([
'intern!object',
'intern/chai!assert',
'require'
], function (registerSuite, assert, require) {
'use strict';
var url = 'http://localhost:3030/complete_reset_password';
registerSuite({
name: 'complete_reset_password',
setup: function () {
},
'open page': function () {
return this.get('remote')
.get(require.toUrl(url))
.waitForElementById('fxa-complete-reset-password-header')
.end();
}
});
});

Просмотреть файл

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define([
'intern!object',
'intern/chai!assert',
'require'
], function (registerSuite, assert, require) {
'use strict';
var PAGE_URL = 'http://localhost:3030/confirm_reset_password';
registerSuite({
name: 'confirm_password_reset',
'open page': function () {
return this.get('remote')
.get(require.toUrl(PAGE_URL))
.waitForElementById('fxa-confirm-reset-password-header')
.end();
}
});
});

Просмотреть файл

@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define([
'intern!object',
'intern/chai!assert',
'require',
'intern/node_modules/dojo/node!xmlhttprequest',
'app/bower_components/fxa-js-client/fxa-client'
], function (registerSuite, assert, require, nodeXMLHttpRequest, FxaClient) {
'use strict';
var AUTH_SERVER_ROOT = 'http://127.0.0.1:9000/v1';
var PAGE_URL = 'http://localhost:3030/reset_password';
var PASSWORD = 'password';
var email;
registerSuite({
name: 'password_reset',
setup: function () {
email = 'signin' + Math.random() + '@example.com';
var client = new FxaClient(AUTH_SERVER_ROOT, {
xhr: nodeXMLHttpRequest.XMLHttpRequest
});
return client.signUp(email, PASSWORD);
},
'open page': function () {
return this.get('remote')
.get(require.toUrl(PAGE_URL))
.waitForElementById('fxa-reset-password-header')
.end();
}
});
});

Просмотреть файл

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define([
'intern!object',
'intern/chai!assert',
'require'
], function (registerSuite, assert, require) {
'use strict';
var url = 'http://localhost:3030/reset_password_complete';
registerSuite({
name: 'reset_password_complete',
setup: function () {
},
'open email verification link': function () {
return this.get('remote')
.get(require.toUrl(url))
.waitForElementById('fxa-reset-password-complete-header')
.end();
}
});
});