Add a password strength component (#4153)
* Added new PasswordStrength Component * Added tests * PR Grumbles
This commit is contained in:
parent
57ce845e4c
commit
4a714d4a3e
@ -189,6 +189,7 @@
|
|||||||
"valid-url": "1.0.9",
|
"valid-url": "1.0.9",
|
||||||
"validator": "6.2.0",
|
"validator": "6.2.0",
|
||||||
"web3": "0.17.0-beta",
|
"web3": "0.17.0-beta",
|
||||||
"whatwg-fetch": "2.0.1"
|
"whatwg-fetch": "2.0.1",
|
||||||
|
"zxcvbn": "4.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
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 { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||||
import ActionAutorenew from 'material-ui/svg-icons/action/autorenew';
|
import ActionAutorenew from 'material-ui/svg-icons/action/autorenew';
|
||||||
|
|
||||||
|
@ -88,12 +88,13 @@ export default class RecoveryPhrase extends Component {
|
|||||||
value={ password2 }
|
value={ password2 }
|
||||||
onChange={ this.onEditPassword2 } />
|
onChange={ this.onEditPassword2 } />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className={ styles.checkbox }
|
className={ styles.checkbox }
|
||||||
label='Key was created with Parity <1.4.5 on Windows'
|
label='Key was created with Parity <1.4.5 on Windows'
|
||||||
checked={ windowsPhrase }
|
checked={ windowsPhrase }
|
||||||
onCheck={ this.onToggleWindowsPhrase } />
|
onCheck={ this.onToggleWindowsPhrase }
|
||||||
</div>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import ActionDone from 'material-ui/svg-icons/action/done';
|
import ActionDone from 'material-ui/svg-icons/action/done';
|
||||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
||||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||||
@ -100,44 +101,45 @@ export default class CreateAccount extends Component {
|
|||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case 0:
|
||||||
return (
|
return (
|
||||||
<CreationType
|
<CreationType onChange={ this.onChangeType } />
|
||||||
onChange={ this.onChangeType } />
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
if (createType === 'fromNew') {
|
if (createType === 'fromNew') {
|
||||||
return (
|
return (
|
||||||
<NewAccount
|
<NewAccount onChange={ this.onChangeDetails } />
|
||||||
onChange={ this.onChangeDetails } />
|
|
||||||
);
|
);
|
||||||
} else if (createType === 'fromGeth') {
|
}
|
||||||
|
|
||||||
|
if (createType === 'fromGeth') {
|
||||||
return (
|
return (
|
||||||
<NewGeth
|
<NewGeth
|
||||||
accounts={ accounts }
|
accounts={ accounts }
|
||||||
onChange={ this.onChangeGeth } />
|
onChange={ this.onChangeGeth }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else if (createType === 'fromPhrase') {
|
}
|
||||||
|
|
||||||
|
if (createType === 'fromPhrase') {
|
||||||
return (
|
return (
|
||||||
<RecoveryPhrase
|
<RecoveryPhrase onChange={ this.onChangeDetails } />
|
||||||
onChange={ this.onChangeDetails } />
|
|
||||||
);
|
);
|
||||||
} else if (createType === 'fromRaw') {
|
}
|
||||||
|
|
||||||
|
if (createType === 'fromRaw') {
|
||||||
return (
|
return (
|
||||||
<RawKey
|
<RawKey onChange={ this.onChangeDetails } />
|
||||||
onChange={ this.onChangeDetails } />
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewImport
|
<NewImport onChange={ this.onChangeWallet } />
|
||||||
onChange={ this.onChangeWallet } />
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
if (createType === 'fromGeth') {
|
if (createType === 'fromGeth') {
|
||||||
return (
|
return (
|
||||||
<AccountDetailsGeth
|
<AccountDetailsGeth addresses={ this.state.gethAddresses } />
|
||||||
addresses={ this.state.gethAddresses } />
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +147,8 @@ export default class CreateAccount extends Component {
|
|||||||
<AccountDetails
|
<AccountDetails
|
||||||
address={ this.state.address }
|
address={ this.state.address }
|
||||||
name={ this.state.name }
|
name={ this.state.name }
|
||||||
phrase={ this.state.phrase } />
|
phrase={ this.state.phrase }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,11 +213,14 @@ export default class CreateAccount extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Warning warning={
|
<Warning
|
||||||
|
warning={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='createAccount.warning.insecurePassword'
|
id='createAccount.warning.insecurePassword'
|
||||||
defaultMessage='It is recommended that a strong password be used to secure your accounts. Empty and trivial passwords are a security risk.' />
|
defaultMessage='It is recommended that a strong password be used to secure your accounts. Empty and trivial passwords are a security risk.'
|
||||||
} />
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +377,7 @@ export default class CreateAccount extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => {
|
onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => {
|
||||||
this.setState({
|
const nextState = {
|
||||||
canCreate,
|
canCreate,
|
||||||
name,
|
name,
|
||||||
passwordHint,
|
passwordHint,
|
||||||
@ -380,7 +386,9 @@ export default class CreateAccount extends Component {
|
|||||||
phrase,
|
phrase,
|
||||||
windowsPhrase: windowsPhrase || false,
|
windowsPhrase: windowsPhrase || false,
|
||||||
rawKey
|
rawKey
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.setState(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeRaw = (canCreate, rawKey) => {
|
onChangeRaw = (canCreate, rawKey) => {
|
||||||
|
@ -23,7 +23,7 @@ import { connect } from 'react-redux';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import { newError, openSnackbar } from '~/redux/actions';
|
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 Form, { Input } from '~/ui/Form';
|
||||||
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
|
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ class PasswordManager extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPage () {
|
renderPage () {
|
||||||
const { busy, isRepeatValid, passwordHint } = this.store;
|
const { busy, isRepeatValid, newPassword, passwordHint } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
@ -236,6 +236,8 @@ class PasswordManager extends Component {
|
|||||||
type='password' />
|
type='password' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PasswordStrength input={ newPassword } />
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
17
js/src/ui/Form/PasswordStrength/index.js
Normal file
17
js/src/ui/Form/PasswordStrength/index.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export default from './passwordStrength';
|
31
js/src/ui/Form/PasswordStrength/passwordStrength.css
Normal file
31
js/src/ui/Form/PasswordStrength/passwordStrength.css
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
125
js/src/ui/Form/PasswordStrength/passwordStrength.js
Normal file
125
js/src/ui/Form/PasswordStrength/passwordStrength.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={ styles.strength }>
|
||||||
|
<label className={ styles.label }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.passwordStrength.label'
|
||||||
|
defaultMessage='password strength'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<LinearProgress
|
||||||
|
color={ color }
|
||||||
|
mode='determinate'
|
||||||
|
style={ BAR_STYLE }
|
||||||
|
value={ value }
|
||||||
|
/>
|
||||||
|
<div className={ styles.feedback }>
|
||||||
|
{ this.renderFeedback(feedback) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{ suggestions.join(' ') }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStrengthBarColor (score) {
|
||||||
|
switch (score) {
|
||||||
|
case 4:
|
||||||
|
case 3:
|
||||||
|
return 'lightgreen';
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return 'yellow';
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return 'orange';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'red';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
js/src/ui/Form/PasswordStrength/passwordStrength.spec.js
Normal file
62
js/src/ui/Form/PasswordStrength/passwordStrength.spec.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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(
|
||||||
|
<PasswordStrength { ...props } />
|
||||||
|
).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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -43,6 +43,7 @@ import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
|
|||||||
import muiTheme from './Theme';
|
import muiTheme from './Theme';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
import ParityBackground from './ParityBackground';
|
import ParityBackground from './ParityBackground';
|
||||||
|
import PasswordStrength from './Form/PasswordStrength';
|
||||||
import ShortenedHash from './ShortenedHash';
|
import ShortenedHash from './ShortenedHash';
|
||||||
import SignerIcon from './SignerIcon';
|
import SignerIcon from './SignerIcon';
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
@ -91,6 +92,7 @@ export {
|
|||||||
muiTheme,
|
muiTheme,
|
||||||
Page,
|
Page,
|
||||||
ParityBackground,
|
ParityBackground,
|
||||||
|
PasswordStrength,
|
||||||
RadioButtons,
|
RadioButtons,
|
||||||
ShortenedHash,
|
ShortenedHash,
|
||||||
Select,
|
Select,
|
||||||
|
Loading…
Reference in New Issue
Block a user