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

Authenticate the user before setting second passphrase - Closes #980 #985

Merged
merged 6 commits into from
Nov 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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