Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #985 from LiskHQ/980-second-pass-pre-authenticate
Browse files Browse the repository at this point in the history
Authenticate the user before setting second passphrase - Closes #980
  • Loading branch information
reyraa committed Nov 17, 2017
2 parents aa9dd5e + 9abf088 commit 52d6be2
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 43 deletions.
66 changes: 66 additions & 0 deletions src/components/authenticate/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { handleChange, authStatePrefill, authStateIsValid } from '../../utils/form';
import ActionBar from '../actionBar';
import AuthInputs from '../authInputs';
import InfoParagraph from '../infoParagraph';

class Authenticate extends React.Component {
constructor() {
super();
this.state = {
...authStatePrefill(),
};
this.message = '';
}

componentDidMount() {
const newState = {
...authStatePrefill(this.props.account),
};
this.setState(newState);
}

componentWillUpdate(props) {
const { nextAction, t } = props;
this.message = `${t('You are looking into a saved account. In order to')} ${t(nextAction)} ${t('you need to enter your passphrase.')}`;
}

update(e) {
e.preventDefault();
const data = {
activePeer: this.props.peers.data,
passphrase: this.state.passphrase.value,
};
if (typeof this.props.account.secondPublicKey === 'string') {
data.secondPassphrase = this.state.secondPassphrase.value;
}
this.props.accountUpdated(data);
}

render() {
return (
<form>
<InfoParagraph>
{this.message}
</InfoParagraph>

<AuthInputs
passphrase={this.state.passphrase}
secondPassphrase={this.state.secondPassphrase}
onChange={handleChange.bind(this)} />

<ActionBar
secondaryButton={{
onClick: this.props.closeDialog,
}}
primaryButton={{
label: this.props.t('Submit'),
onClick: this.update.bind(this),
className: 'authenticate-button',
disabled: (!authStateIsValid(this.state)),
}} />
</form>);
}
}

export default Authenticate;
90 changes: 90 additions & 0 deletions src/components/authenticate/authenticate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import PropTypes from 'prop-types';
import { spy } from 'sinon';
import ActionBar from '../actionBar';
import i18n from '../../i18n';
import Authenticate from './authenticate';


const fakeStore = configureStore();

describe('Authenticate', () => {
let wrapper;
let props;

const peers = {
status: {
online: false,
},
data: {
currentPeer: 'localhost',
port: 4000,
options: {
name: 'Custom Node',
},
},
};

const account = {
isDelegate: false,
publicKey: 'c094ebee7ec0c50ebee32918655e089f6e1a604b83bcaa760293c61e0f18ab6f',
address: '16313739661670634666L',
};

const passphrase = 'wagon stock borrow episode laundry kitten salute link globe zero feed marble';

beforeEach(() => {
props = {
account,
peers,
t: str => str,
nextAction: 'perform a sample action',
closeDialog: spy(),
accountUpdated: spy(),
};

const store = fakeStore({
account: {
balance: 100e8,
},
});
wrapper = mount(<Authenticate {...props} />, {
context: { store, i18n },
childContextTypes: {
store: PropTypes.object.isRequired,
i18n: PropTypes.object.isRequired,
},
});
});

it('renders 3 compound React components', () => {
expect(wrapper.find('InfoParagraph')).to.have.length(1);
expect(wrapper.find(ActionBar)).to.have.length(1);
expect(wrapper.find('AuthInputs')).to.have.length(1);
});

it('should render InfoParagraph with appropriate message', () => {
expect(wrapper.find('InfoParagraph').text()).to.include(
`You are looking into a saved account. In order to ${props.nextAction} you need to enter your passphrase`);
});

it('should activate primary button if correct passphrase entered', () => {
expect(wrapper.find('button.authenticate-button').props().disabled).to.equal(true);
wrapper.find('.passphrase input').simulate('change', { target: { value: passphrase } });
expect(wrapper.find('button.authenticate-button').props().disabled).to.equal(false);
});

it('should call accountUpdated if entered passphrase and clicked submit', () => {
wrapper.find('.passphrase input').simulate('change', { target: { value: passphrase } });
wrapper.update();
wrapper.find('Button.authenticate-button').simulate('click');
wrapper.update();
expect(props.accountUpdated).to.have.been.calledWith({
activePeer: props.peers.data,
passphrase,
});
});
});
21 changes: 21 additions & 0 deletions src/components/authenticate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import { accountUpdated } from '../../actions/account';
import Authenticate from './authenticate';

/**
* Passing state
*/
const mapStateToProps = state => ({
peers: state.peers,
account: state.account,
});

const mapDispatchToProps = dispatch => ({
accountUpdated: data => dispatch(accountUpdated(data)),
});

export default connect(
mapStateToProps,
mapDispatchToProps,
)(translate()(Authenticate));
49 changes: 49 additions & 0 deletions src/components/authenticate/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import i18n from '../../i18n';
import AuthenticateHOC from './index';

describe('AuthenticateHOC', () => {
let wrapper;
const peers = {
status: {
online: false,
},
data: {
currentPeer: 'localhost',
port: 4000,
options: {
name: 'Custom Node',
},
},
};

const account = {
isDelegate: false,
address: '16313739661670634666L',
username: 'lisk-nano',
};

const store = configureMockStore([])({
peers,
account,
});

beforeEach(() => {
wrapper = mount(<Provider store={store}><AuthenticateHOC i18n={i18n} /></Provider>);
});

it('should render Authenticate', () => {
expect(wrapper.find('Authenticate')).to.have.lengthOf(1);
});

it('should mount Authenticate with appropriate properties', () => {
const props = wrapper.find('Authenticate').props();
expect(props.peers).to.be.equal(peers);
expect(props.account).to.be.equal(account);
expect(typeof props.accountUpdated).to.be.equal('function');
});
});
4 changes: 2 additions & 2 deletions src/components/registerDelegate/registerDelegate.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ class RegisterDelegate extends React.Component {
<Input label={this.props.t('Delegate name')} required={true}
autoFocus={true}
className='username'
onChange={handleChange.bind(this, this, 'name')}
onChange={handleChange.bind(this, 'name')}
error={this.state.name.error}
value={this.state.name.value} />
<AuthInputs
passphrase={this.state.passphrase}
secondPassphrase={this.state.secondPassphrase}
onChange={handleChange.bind(this, this)} />
onChange={handleChange.bind(this)} />
<hr/>
<InfoParagraph>
{this.props.t('Becoming a delegate requires registration. You may choose your own delegate name, which can be used to promote your delegate. Only the top 101 delegates are eligible to forge. All fees are shared equally between the top 101 delegates.')}
Expand Down
16 changes: 14 additions & 2 deletions src/components/relativeLink/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router';
import buttonStyle from 'react-toolbox/lib/button/theme.css';
import offlineStyle from '../offlineWrapper/offlineWrapper.css';
import dialogs from '../dialog/dialogs';

const RelativeLink = ({
location, to, children, className, raised, neutral, primary, flat, disableWhenOffline,
Expand All @@ -15,10 +17,20 @@ const RelativeLink = ({
if (disableWhenOffline !== undefined) style += `${offlineStyle.disableWhenOffline} `;
if (style !== '') style += ` ${buttonStyle.button}`;

const path = location.pathname.indexOf(`/${to}`) < 0 ? `${location.pathname}/${to}`.replace('//', '/') : location.pathname;
const dialogNames = Object.keys(dialogs());
let pathname = location.pathname;
dialogNames.forEach((dialog) => {
pathname = pathname.replace(`/${dialog}`, '');
});

const path = `${pathname}/${to}`.replace('//', '/');
return (
<Link className={`${className} ${style}`} to={path}>{ children }</Link>
);
};

export default withRouter(RelativeLink);
const mapStateToProps = state => ({
dialog: state.dialog,
});

export default withRouter(connect(mapStateToProps)(RelativeLink));
1 change: 1 addition & 0 deletions src/components/secondPassphrase/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SecondPassphrase from './secondPassphrase';
*/
const mapStateToProps = state => ({
account: state.account,
passphrase: state.account.passphrase,
peers: state.peers,
});

Expand Down
23 changes: 13 additions & 10 deletions src/components/secondPassphrase/secondPassphrase.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import Passphrase from '../passphrase';
import Fees from '../../constants/fees';
import Authenticate from '../authenticate';

const SecondPassphrase = ({
account, peers, registerSecondPassphrase, closeDialog, t,
passphrase, account, peers, registerSecondPassphrase, closeDialog, t,
}) => {
const onLoginSubmission = (secondPassphrase) => {
registerSecondPassphrase({
Expand All @@ -14,15 +15,17 @@ const SecondPassphrase = ({
};

return (
<Passphrase
onPassGenerated={onLoginSubmission}
keepModal={true}
fee={Fees.setSecondPassphrase}
closeDialog={closeDialog}
confirmButton={t('Register')}
useCaseNote={t('your second passphrase will be required for all transactions sent from this account')}
securityNote={t('Losing access to this passphrase will mean no funds can be sent from this account.')}/>
);
typeof passphrase === 'string' && passphrase.length > 0 ?
<Passphrase
onPassGenerated={onLoginSubmission}
keepModal={true}
fee={Fees.setSecondPassphrase}
closeDialog={closeDialog}
confirmButton={t('Register')}
useCaseNote={t('your second passphrase will be required for all transactions sent from this account')}
securityNote={t('Losing access to this passphrase will mean no funds can be sent from this account.')}/>
:
<Authenticate nextAction='set second passphrase'/>);
};

export default SecondPassphrase;
Loading

0 comments on commit 52d6be2

Please sign in to comment.