Add Check and Change Password for an Account (#2861)

* Added new RPC endpoints to JSAPI (#2389)

* Added modal in Account Page to test & modify password (#2389)

* Modify hint with password change // Better tabs (#2556)
This commit is contained in:
Nicolas Gotchac 2016-10-25 17:54:01 +02:00 committed by Gav Wood
parent e71c752210
commit 2d2e9c4d6e
9 changed files with 550 additions and 10 deletions

View File

@ -33,6 +33,11 @@ export default class Personal {
.execute('personal_confirmRequest', inNumber16(requestId), options, password); .execute('personal_confirmRequest', inNumber16(requestId), options, password);
} }
changePassword (account, password, newPassword) {
return this._transport
.execute('personal_changePassword', inAddress(account), password, newPassword);
}
generateAuthorizationToken () { generateAuthorizationToken () {
return this._transport return this._transport
.execute('personal_generateAuthorizationToken'); .execute('personal_generateAuthorizationToken');
@ -105,6 +110,11 @@ export default class Personal {
.execute('personal_signerEnabled'); .execute('personal_signerEnabled');
} }
testPassword (account, password) {
return this._transport
.execute('personal_testPassword', inAddress(account), password);
}
unlockAccount (account, password, duration = 1) { unlockAccount (account, password, duration = 1) {
return this._transport return this._transport
.execute('personal_unlockAccount', inAddress(account), password, inNumber10(duration)); .execute('personal_unlockAccount', inAddress(account), password, inNumber10(duration));

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './passwordManager.js';

View File

@ -0,0 +1,73 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.accountContainer {
display: flex;
flex-direction: row;
margin-bottom: 1.5rem;
}
.accountInfos {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.accountInfos > * {
margin: 0.25rem 0;
}
.hintLabel {
text-transform: uppercase;
font-size: 0.7rem;
margin-right: 0.5rem;
}
.accountAddress {
font-family: monospace;
font-size: 0.9rem;
}
.accountName {
font-size: 1.1rem;
}
.passwordHint {
font-size: 0.9rem;
color: lightgrey;
}
.message {
margin-top: 1rem;
width: 100%;
height: 2.5rem;
text-align: center;
line-height: 2.5rem;
transition: height 350ms 0 !important;
overflow: hidden;
}
.hideMessage {
height: 0;
background-color: transparent !important;
}
.form {
margin-top: 0;
padding: 0 0.5rem 1rem;
background-color: rgba(255, 255, 255, 0.05);
}

View File

@ -0,0 +1,383 @@
// Copyright 2015, 2016 Ethcore (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 ContentClear from 'material-ui/svg-icons/content/clear';
import CheckIcon from 'material-ui/svg-icons/navigation/check';
import SendIcon from 'material-ui/svg-icons/content/send';
import { Tabs, Tab } from 'material-ui/Tabs';
import Paper from 'material-ui/Paper';
import Form, { Input } from '../../ui/Form';
import { Button, Modal, IdentityName, IdentityIcon } from '../../ui';
import styles from './passwordManager.css';
const TEST_ACTION = 'TEST_ACTION';
const CHANGE_ACTION = 'CHANGE_ACTION';
export default class PasswordManager extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
account: PropTypes.object.isRequired,
onClose: PropTypes.func
}
state = {
action: TEST_ACTION,
waiting: false,
showMessage: false,
message: { value: '', success: true },
currentPass: '',
newPass: '',
repeatNewPass: '',
repeatValid: true,
passwordHint: this.props.account.meta && this.props.account.meta.passwordHint || ''
}
render () {
return (
<Modal
actions={ this.renderDialogActions() }
title='Password Manager'
visible>
{ this.renderAccount() }
{ this.renderPage() }
{ this.renderMessage() }
</Modal>
);
}
renderMessage () {
const { message, showMessage } = this.state;
const style = message.success
? {
backgroundColor: 'rgba(174, 213, 129, 0.75)'
}
: {
backgroundColor: 'rgba(229, 115, 115, 0.75)'
};
const classes = [ styles.message ];
if (!showMessage) {
classes.push(styles.hideMessage);
}
return (
<Paper
zDepth={ 1 }
style={ style }
className={ classes.join(' ') }>
{ message.value }
</Paper>
);
}
renderAccount () {
const { account } = this.props;
const { address, meta } = account;
const passwordHint = meta && meta.passwordHint
? (
<span className={ styles.passwordHint }>
<span className={ styles.hintLabel }>Hint </span>
{ meta.passwordHint }
</span>
)
: null;
return (
<div className={ styles.accountContainer }>
<IdentityIcon
address={ address }
/>
<div className={ styles.accountInfos }>
<IdentityName
className={ styles.accountName }
address={ address }
unknown
/>
<span className={ styles.accountAddress }>
{ address }
</span>
{ passwordHint }
</div>
</div>
);
}
renderPage () {
const { account } = this.props;
const { waiting, repeatValid } = this.state;
const disabled = !!waiting;
const repeatError = repeatValid
? null
: 'the two passwords differ';
const { meta } = account;
const passwordHint = meta && meta.passwordHint || '';
return (
<Tabs
inkBarStyle={ {
backgroundColor: 'rgba(255, 255, 255, 0.55)'
} }
tabItemContainerStyle={ {
backgroundColor: 'rgba(255, 255, 255, 0.05)'
} }
>
<Tab
onActive={ this.handleTestActive }
label='Test Password'
>
<Form
className={ styles.form }
>
<div>
<Input
label='password'
hint='your current password for this account'
type='password'
submitOnBlur={ false }
disabled={ disabled }
onSubmit={ this.handleTestPassword }
onChange={ this.onEditCurrent } />
</div>
</Form>
</Tab>
<Tab
onActive={ this.handleChangeActive }
label='Change Password'
>
<Form
className={ styles.form }
>
<div>
<Input
label='current password'
hint='your current password for this account'
type='password'
submitOnBlur={ false }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditCurrent } />
<Input
label='new password'
hint='the new password for this account'
type='password'
submitOnBlur={ false }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditNew } />
<Input
label='repeat new password'
hint='repeat the new password for this account'
type='password'
submitOnBlur={ false }
error={ repeatError }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditRepeatNew } />
<Input
label='new password hint'
hint='hint for the new password'
submitOnBlur={ false }
value={ passwordHint }
disabled={ disabled }
onSubmit={ this.handleChangePassword }
onChange={ this.onEditHint } />
</div>
</Form>
</Tab>
</Tabs>
);
}
renderDialogActions () {
const { onClose } = this.props;
const { action, waiting, repeatValid } = this.state;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
onClick={ onClose } />
);
if (waiting) {
const waitingBtn = (
<Button
disabled
label='Wait...' />
);
return [ cancelBtn, waitingBtn ];
}
if (action === TEST_ACTION) {
const testBtn = (
<Button
icon={ <CheckIcon /> }
label='Test'
onClick={ this.handleTestPassword } />
);
return [ cancelBtn, testBtn ];
}
const changeBtn = (
<Button
disabled={ !repeatValid }
icon={ <SendIcon /> }
label='Change'
onClick={ this.handleChangePassword } />
);
return [ cancelBtn, changeBtn ];
}
onEditCurrent = (event, value) => {
this.setState({
currentPass: value,
showMessage: false
});
}
onEditNew = (event, value) => {
const repeatValid = value === this.state.repeatNewPass;
this.setState({
newPass: value,
showMessage: false,
repeatValid
});
}
onEditRepeatNew = (event, value) => {
const repeatValid = value === this.state.newPass;
this.setState({
repeatNewPass: value,
showMessage: false,
repeatValid
});
}
onEditHint = (event, value) => {
this.setState({
passwordHint: value,
showMessage: false
});
}
handleTestActive = () => {
this.setState({
action: TEST_ACTION,
showMessage: false
});
}
handleChangeActive = () => {
this.setState({
action: CHANGE_ACTION,
showMessage: false
});
}
handleTestPassword = () => {
const { account } = this.props;
const { currentPass } = this.state;
this.setState({ waiting: true, showMessage: false });
this.context
.api.personal
.testPassword(account.address, currentPass)
.then(correct => {
const message = correct
? { value: 'This password is correct', success: true }
: { value: 'This password is not correct', success: false };
this.setState({ waiting: false, message, showMessage: true });
})
.catch(e => {
console.error('passwordManager::handleTestPassword', e);
this.setState({ waiting: false });
});
}
handleChangePassword = () => {
const { account } = this.props;
const { currentPass, newPass, repeatNewPass, passwordHint } = this.state;
if (repeatNewPass !== newPass) {
return;
}
this.setState({ waiting: true, showMessage: false });
this.context
.api.personal
.testPassword(account.address, currentPass)
.then(correct => {
if (!correct) {
const message = {
value: 'This provided current password is not correct',
success: false
};
this.setState({ waiting: false, message, showMessage: true });
return false;
}
const meta = Object.assign({}, account.meta, {
passwordHint
});
return Promise.all([
this.context
.api.personal
.setAccountMeta(account.address, meta),
this.context
.api.personal
.changePassword(account.address, currentPass, newPass)
])
.then(() => {
const message = {
value: 'Your password has been successfully changed',
success: true
};
this.setState({ waiting: false, message, showMessage: true });
});
})
.catch(e => {
console.error('passwordManager::handleChangePassword', e);
this.setState({ waiting: false });
});
}
}

View File

@ -23,6 +23,7 @@ import ExecuteContract from './ExecuteContract';
import FirstRun from './FirstRun'; import FirstRun from './FirstRun';
import Shapeshift from './Shapeshift'; import Shapeshift from './Shapeshift';
import Transfer from './Transfer'; import Transfer from './Transfer';
import PasswordManager from './PasswordManager';
export { export {
AddAddress, AddAddress,
@ -33,5 +34,6 @@ export {
ExecuteContract, ExecuteContract,
FirstRun, FirstRun,
Shapeshift, Shapeshift,
Transfer Transfer,
PasswordManager
}; };

View File

@ -44,13 +44,18 @@ export default class Input extends Component {
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
rows: PropTypes.number, rows: PropTypes.number,
type: PropTypes.string, type: PropTypes.string,
submitOnBlur: PropTypes.bool,
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.number, PropTypes.string PropTypes.number, PropTypes.string
]) ])
} }
static defaultProps = {
submitOnBlur: true
}
state = { state = {
value: this.props.value value: this.props.value || ''
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
@ -97,8 +102,11 @@ export default class Input extends Component {
onBlur = (event) => { onBlur = (event) => {
const { value } = event.target; const { value } = event.target;
const { submitOnBlur } = this.props;
this.onSubmit(value); if (submitOnBlur) {
this.onSubmit(value);
}
this.props.onBlur && this.props.onBlur(event); this.props.onBlur && this.props.onBlur(event);
} }

View File

@ -20,15 +20,23 @@ import styles from './form.css';
export default class Form extends Component { export default class Form extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node children: PropTypes.node,
className: PropTypes.string
} }
render () { render () {
const { className } = this.props;
const classes = [ styles.form ];
if (className) {
classes.push(className);
}
// HACK: hidden inputs to disable Chrome's autocomplete // HACK: hidden inputs to disable Chrome's autocomplete
return ( return (
<form <form
autoComplete='new-password' autoComplete='new-password'
className={ styles.form }> className={ classes.join(' ') }>
<div className={ styles.autofill }> <div className={ styles.autofill }>
<input type='text' name='fakeusernameremembered' /> <input type='text' name='fakeusernameremembered' />
<input type='password' name='fakepasswordremembered' /> <input type='password' name='fakepasswordremembered' />

View File

@ -22,6 +22,7 @@ const defaultName = 'UNNAMED';
class IdentityName extends Component { class IdentityName extends Component {
static propTypes = { static propTypes = {
className: PropTypes.string,
address: PropTypes.string, address: PropTypes.string,
accountsInfo: PropTypes.object, accountsInfo: PropTypes.object,
tokens: PropTypes.object, tokens: PropTypes.object,
@ -31,7 +32,7 @@ class IdentityName extends Component {
} }
render () { render () {
const { address, accountsInfo, tokens, empty, shorten, unknown } = this.props; const { address, accountsInfo, tokens, empty, shorten, unknown, className } = this.props;
const account = accountsInfo[address] || tokens[address]; const account = accountsInfo[address] || tokens[address];
const hasAccount = account && (!account.meta || !account.meta.deleted); const hasAccount = account && (!account.meta || !account.meta.deleted);
@ -47,7 +48,9 @@ class IdentityName extends Component {
: fallback; : fallback;
return ( return (
<span>{ name && name.length ? name : fallback }</span> <span className={ className }>
{ name && name.length ? name : fallback }
</span>
); );
} }

View File

@ -19,8 +19,9 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import ContentCreate from 'material-ui/svg-icons/content/create'; import ContentCreate from 'material-ui/svg-icons/content/create';
import ContentSend from 'material-ui/svg-icons/content/send'; import ContentSend from 'material-ui/svg-icons/content/send';
import LockIcon from 'material-ui/svg-icons/action/lock';
import { EditMeta, Shapeshift, Transfer } from '../../modals'; import { EditMeta, Shapeshift, Transfer, PasswordManager } from '../../modals';
import { Actionbar, Button, Page } from '../../ui'; import { Actionbar, Button, Page } from '../../ui';
import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png'; import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png';
@ -44,7 +45,8 @@ class Account extends Component {
state = { state = {
showEditDialog: false, showEditDialog: false,
showFundDialog: false, showFundDialog: false,
showTransferDialog: false showTransferDialog: false,
showPasswordDialog: false
} }
render () { render () {
@ -63,6 +65,7 @@ class Account extends Component {
{ this.renderEditDialog(account) } { this.renderEditDialog(account) }
{ this.renderFundDialog() } { this.renderFundDialog() }
{ this.renderTransferDialog() } { this.renderTransferDialog() }
{ this.renderPasswordDialog() }
{ this.renderActionbar() } { this.renderActionbar() }
<Page> <Page>
<Header <Header
@ -93,7 +96,12 @@ class Account extends Component {
key='editmeta' key='editmeta'
icon={ <ContentCreate /> } icon={ <ContentCreate /> }
label='edit' label='edit'
onClick={ this.onEditClick } /> onClick={ this.onEditClick } />,
<Button
key='passwordManager'
icon={ <LockIcon /> }
label='password'
onClick={ this.onPasswordClick } />
]; ];
return ( return (
@ -156,6 +164,24 @@ class Account extends Component {
); );
} }
renderPasswordDialog () {
const { showPasswordDialog } = this.state;
if (!showPasswordDialog) {
return null;
}
const { address } = this.props.params;
const { accounts } = this.props;
const account = accounts[address];
return (
<PasswordManager
account={ account }
onClose={ this.onPasswordClose } />
);
}
onEditClick = () => { onEditClick = () => {
this.setState({ this.setState({
showEditDialog: !this.state.showEditDialog showEditDialog: !this.state.showEditDialog
@ -181,6 +207,16 @@ class Account extends Component {
onTransferClose = () => { onTransferClose = () => {
this.onTransferClick(); this.onTransferClick();
} }
onPasswordClick = () => {
this.setState({
showPasswordDialog: !this.state.showPasswordDialog
});
}
onPasswordClose = () => {
this.onPasswordClick();
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {