From 4a714d4a3ef38b2529ca2fd3675ebb233d2f70ad Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 13 Jan 2017 15:52:42 +0100 Subject: [PATCH] Add a password strength component (#4153) * Added new PasswordStrength Component * Added tests * PR Grumbles --- js/package.json | 3 +- .../CreateAccount/NewAccount/newAccount.js | 2 +- .../RecoveryPhrase/recoveryPhrase.js | 11 +- js/src/modals/CreateAccount/createAccount.js | 56 ++++---- .../modals/PasswordManager/passwordManager.js | 6 +- js/src/ui/Form/PasswordStrength/index.js | 17 +++ .../PasswordStrength/passwordStrength.css | 31 +++++ .../Form/PasswordStrength/passwordStrength.js | 125 ++++++++++++++++++ .../PasswordStrength/passwordStrength.spec.js | 62 +++++++++ js/src/ui/index.js | 2 + 10 files changed, 282 insertions(+), 33 deletions(-) create mode 100644 js/src/ui/Form/PasswordStrength/index.js create mode 100644 js/src/ui/Form/PasswordStrength/passwordStrength.css create mode 100644 js/src/ui/Form/PasswordStrength/passwordStrength.js create mode 100644 js/src/ui/Form/PasswordStrength/passwordStrength.spec.js diff --git a/js/package.json b/js/package.json index 3a115b5c0..f8a4b2c67 100644 --- a/js/package.json +++ b/js/package.json @@ -189,6 +189,7 @@ "valid-url": "1.0.9", "validator": "6.2.0", "web3": "0.17.0-beta", - "whatwg-fetch": "2.0.1" + "whatwg-fetch": "2.0.1", + "zxcvbn": "4.4.1" } } diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.js b/js/src/modals/CreateAccount/NewAccount/newAccount.js index ed2c24612..cbf7d1587 100644 --- a/js/src/modals/CreateAccount/NewAccount/newAccount.js +++ b/js/src/modals/CreateAccount/NewAccount/newAccount.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; -import IconButton from 'material-ui/IconButton'; +import { IconButton } from 'material-ui'; import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; import ActionAutorenew from 'material-ui/svg-icons/action/autorenew'; diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js index dc80e27ae..9d76cebfa 100644 --- a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js +++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js @@ -88,12 +88,13 @@ export default class RecoveryPhrase extends Component { value={ password2 } onChange={ this.onEditPassword2 } /> - + ); } diff --git a/js/src/modals/CreateAccount/createAccount.js b/js/src/modals/CreateAccount/createAccount.js index 53be1f918..e1e6abc71 100644 --- a/js/src/modals/CreateAccount/createAccount.js +++ b/js/src/modals/CreateAccount/createAccount.js @@ -16,6 +16,7 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; + import ActionDone from 'material-ui/svg-icons/action/done'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ContentClear from 'material-ui/svg-icons/content/clear'; @@ -100,44 +101,45 @@ export default class CreateAccount extends Component { switch (stage) { case 0: return ( - + ); case 1: if (createType === 'fromNew') { return ( - + ); - } else if (createType === 'fromGeth') { + } + + if (createType === 'fromGeth') { return ( + onChange={ this.onChangeGeth } + /> ); - } else if (createType === 'fromPhrase') { + } + + if (createType === 'fromPhrase') { return ( - + ); - } else if (createType === 'fromRaw') { + } + + if (createType === 'fromRaw') { return ( - + ); } return ( - + ); case 2: if (createType === 'fromGeth') { return ( - + ); } @@ -145,7 +147,8 @@ export default class CreateAccount extends Component { + phrase={ this.state.phrase } + /> ); } } @@ -210,11 +213,14 @@ export default class CreateAccount extends Component { } return ( - - } /> + + } + /> ); } @@ -371,7 +377,7 @@ export default class CreateAccount extends Component { } onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => { - this.setState({ + const nextState = { canCreate, name, passwordHint, @@ -380,7 +386,9 @@ export default class CreateAccount extends Component { phrase, windowsPhrase: windowsPhrase || false, rawKey - }); + }; + + this.setState(nextState); } onChangeRaw = (canCreate, rawKey) => { diff --git a/js/src/modals/PasswordManager/passwordManager.js b/js/src/modals/PasswordManager/passwordManager.js index a1d3f5be3..1bd3ea91c 100644 --- a/js/src/modals/PasswordManager/passwordManager.js +++ b/js/src/modals/PasswordManager/passwordManager.js @@ -23,7 +23,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { newError, openSnackbar } from '~/redux/actions'; -import { Button, Modal, IdentityName, IdentityIcon } from '~/ui'; +import { Button, Modal, IdentityName, IdentityIcon, PasswordStrength } from '~/ui'; import Form, { Input } from '~/ui/Form'; import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons'; @@ -120,7 +120,7 @@ class PasswordManager extends Component { } renderPage () { - const { busy, isRepeatValid, passwordHint } = this.store; + const { busy, isRepeatValid, newPassword, passwordHint } = this.store; return ( + + diff --git a/js/src/ui/Form/PasswordStrength/index.js b/js/src/ui/Form/PasswordStrength/index.js new file mode 100644 index 000000000..54e3c3d3e --- /dev/null +++ b/js/src/ui/Form/PasswordStrength/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './passwordStrength'; diff --git a/js/src/ui/Form/PasswordStrength/passwordStrength.css b/js/src/ui/Form/PasswordStrength/passwordStrength.css new file mode 100644 index 000000000..6512f3afe --- /dev/null +++ b/js/src/ui/Form/PasswordStrength/passwordStrength.css @@ -0,0 +1,31 @@ +/* Copyright 2015, 2016 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.strength { + margin-top: 1.25em; +} + +.feedback { + font-size: 0.75em; +} + +.label { + user-select: none; + line-height: 18px; + font-size: 12px; + color: rgba(255, 255, 255, 0.498039); +} diff --git a/js/src/ui/Form/PasswordStrength/passwordStrength.js b/js/src/ui/Form/PasswordStrength/passwordStrength.js new file mode 100644 index 000000000..6bfe681b9 --- /dev/null +++ b/js/src/ui/Form/PasswordStrength/passwordStrength.js @@ -0,0 +1,125 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import { debounce } from 'lodash'; +import { LinearProgress } from 'material-ui'; +import { FormattedMessage } from 'react-intl'; +import zxcvbn from 'zxcvbn'; + +import styles from './passwordStrength.css'; + +const BAR_STYLE = { + borderRadius: 1, + height: 7, + marginTop: '0.5em' +}; + +export default class PasswordStrength extends Component { + static propTypes = { + input: PropTypes.string.isRequired + }; + + state = { + strength: null + }; + + constructor (props) { + super(props); + + this.updateStrength = debounce(this._updateStrength, 50, { leading: true }); + } + + componentWillMount () { + this.updateStrength(this.props.input); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.input !== this.props.input) { + this.updateStrength(nextProps.input); + } + } + + _updateStrength (input = '') { + const strength = zxcvbn(input); + this.setState({ strength }); + } + + render () { + const { strength } = this.state; + + if (!strength) { + return null; + } + + const { score, feedback } = strength; + + // Score is between 0 and 4 + const value = score * 100 / 5 + 20; + const color = this.getStrengthBarColor(score); + + return ( +
+ + +
+ { this.renderFeedback(feedback) } +
+
+ ); + } + + // Note that the suggestions are in english, thus it wouldn't + // make sense to add translations to surrounding words + renderFeedback (feedback = {}) { + const { suggestions = [] } = feedback; + + return ( +
+

+ { suggestions.join(' ') } +

+
+ ); + } + + getStrengthBarColor (score) { + switch (score) { + case 4: + case 3: + return 'lightgreen'; + + case 2: + return 'yellow'; + + case 1: + return 'orange'; + + default: + return 'red'; + } + } +} diff --git a/js/src/ui/Form/PasswordStrength/passwordStrength.spec.js b/js/src/ui/Form/PasswordStrength/passwordStrength.spec.js new file mode 100644 index 000000000..ac616a87b --- /dev/null +++ b/js/src/ui/Form/PasswordStrength/passwordStrength.spec.js @@ -0,0 +1,62 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; + +import PasswordStrength from './passwordStrength'; + +const INPUT_A = 'l33t_test'; +const INPUT_B = 'Fu£dk;s$%kdlaOe9)_'; +const INPUT_NULL = ''; + +function render (props) { + return shallow( + + ).shallow(); +} + +describe('ui/Form/PasswordStrength', () => { + describe('rendering', () => { + it('renders', () => { + expect(render({ input: INPUT_A })).to.be.ok; + }); + + it('renders a linear progress', () => { + expect(render({ input: INPUT_A }).find('LinearProgress')).to.be.ok; + }); + + describe('compute strength', () => { + it('has low score with empty input', () => { + expect( + render({ input: INPUT_NULL }).find('LinearProgress').props().value + ).to.equal(20); + }); + + it('has medium score', () => { + expect( + render({ input: INPUT_A }).find('LinearProgress').props().value + ).to.equal(60); + }); + + it('has high score', () => { + expect( + render({ input: INPUT_B }).find('LinearProgress').props().value + ).to.equal(100); + }); + }); + }); +}); diff --git a/js/src/ui/index.js b/js/src/ui/index.js index da61cc9b9..79a7c7f7a 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -43,6 +43,7 @@ import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal'; import muiTheme from './Theme'; import Page from './Page'; import ParityBackground from './ParityBackground'; +import PasswordStrength from './Form/PasswordStrength'; import ShortenedHash from './ShortenedHash'; import SignerIcon from './SignerIcon'; import Tags from './Tags'; @@ -91,6 +92,7 @@ export { muiTheme, Page, ParityBackground, + PasswordStrength, RadioButtons, ShortenedHash, Select,