Skip to content

Commit

Permalink
Merge pull request #205 from creative-commoners/pulls/4.0/disable-option
Browse files Browse the repository at this point in the history
NEW Support displaying and setting a default registered method in the member settings fields
  • Loading branch information
Garion Herman committed Jun 24, 2019
2 parents 036b0ae + cbe2334 commit 0ab8bdc
Show file tree
Hide file tree
Showing 23 changed files with 593 additions and 82 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle-cms.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/lang/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
"MultiFactorAuthentication.ADD_ANOTHER_METHOD": "Add another MFA method",
"MultiFactorAuthentication.ADD_FIRST_METHOD": "Add an MFA method",
"MultiFactorAuthentication.REGISTERED": "{method}: Registered",
"MultiFactorAuthentication.DEFAULT_REGISTERED": "{method} (default): Registered",
"MultiFactorAuthentication.BACKUP_REGISTERED": "{method}: Created {date}",
"MultiFactorAuthentication.RESET_METHOD": "Reset",
"MultiFactorAuthentication.REMOVE_METHOD": "Remove",
"MultiFactorAuthentication.SET_AS_DEFAULT": "Set as default method",
"MultiFactorAuthentication.NO_METHODS_REGISTERED": "No MFA methods have been registered. Add one using the button below",
"MultiFactorAuthentication.NO_METHODS_REGISTERED_READONLY": "This member has not registered any MFA methods yet",
"MultiFactorAuthentication.SELECT_METHOD": "Select a verification method",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,49 @@ import classNames from 'classnames';
import moment from 'moment';
import Remove from './MethodListItem/Remove';
import Reset from './MethodListItem/Reset';
import SetDefault from './MethodListItem/SetDefault';
import methodShape from 'types/registeredMethod';

const fallbacks = require('../../../../lang/src/en.json');

/**
* Renders a single Registered MFA Method for a Member
*
* @todo Add actions when not in read-only mode
* @param {object} method
* @param {string} suffix
* @returns {HTMLElement}
* @constructor
*/

class MethodListItem extends PureComponent {
getNameSuffix() {
/**
* Get the status message template for the method item, depending on whether
* it is the default, backup, or a regular method.
*
* @returns {string}
*/
getStatusMessage() {
const { isBackupMethod, isDefaultMethod } = this.props;
const { ss: { i18n } } = window;
const { isDefaultMethod } = this.props;

let suffix = '';
if (isDefaultMethod) {
suffix = i18n._t(
'MultiFactorAuthentication.DEFAULT',
fallbacks['MultiFactorAuthentication.DEFAULT']
return i18n._t(
'MultiFactorAuthentication.DEFAULT_REGISTERED',
fallbacks['MultiFactorAuthentication.DEFAULT_REGISTERED']
);
}
if (suffix.length) {
suffix = ` ${suffix}`;

if (isBackupMethod) {
return i18n._t(
'MultiFactorAuthentication.BACKUP_REGISTERED',
fallbacks['MultiFactorAuthentication.BACKUP_REGISTERED']
);
}

return suffix;
return i18n._t(
'MultiFactorAuthentication.REGISTERED',
fallbacks['MultiFactorAuthentication.REGISTERED']
);
}

renderRemove() {
Expand Down Expand Up @@ -86,6 +98,22 @@ class MethodListItem extends PureComponent {
return <Reset {...props} />;
}

/**
* Renders a button to make the current method the default registered method
*
* @returns {SetDefault}
*/
renderSetAsDefault() {
const { isDefaultMethod, isBackupMethod, method } = this.props;

if (isDefaultMethod || isBackupMethod) {
return null;
}

return <SetDefault method={method} />;
}


renderControls() {
const { canRemove, canReset } = this.props;

Expand All @@ -97,30 +125,26 @@ class MethodListItem extends PureComponent {
<div>
{ this.renderRemove() }
{ this.renderReset() }
{ this.renderSetAsDefault() }
</div>
);
}

/**
* Gets the method name and status, including whether it's default, backup, etc
*
* @returns {string}
*/
renderNameAndStatus() {
const { method, isBackupMethod, createdDate } = this.props;
const { method, createdDate } = this.props;
const { ss: { i18n } } = window;

let statusMessage = i18n._t(
'MultiFactorAuthentication.REGISTERED',
fallbacks['MultiFactorAuthentication.REGISTERED']
);

if (isBackupMethod) {
statusMessage = i18n._t(
'MultiFactorAuthentication.BACKUP_CREATED',
fallbacks['MultiFactorAuthentication.BACKUP_REGISTERED']
);
}
const statusMessage = this.getStatusMessage();

moment.locale(i18n.detectLocale());

return i18n.inject(statusMessage, {
method: `${method.name}${this.getNameSuffix()}`,
method: method.name,
date: moment(createdDate).format('L'),
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const Remove = ({
const token = Config.get('SecurityID');
const endpoint = `${remove.replace('{urlSegment}', method.urlSegment)}?SecurityID=${token}`;

fetch(endpoint).then(response => response.json().then(json => {
fetch(endpoint, {
method: 'DELETE',
}).then(response => response.json().then(json => {
if (response.status === 200) {
onDeregisterMethod(method);
onAddAvailableMethod(json.availableMethod);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import registeredMethodShape from 'types/registeredMethod';
import Config from 'lib/Config'; // eslint-disable-line
import { connect } from 'react-redux';
import { setDefaultMethod } from 'state/mfaAdministration/actions';

const fallbacks = require('../../../../../lang/src/en.json');

/**
* An action to set the current method as the default registered method for a user
*/
class SetDefault extends Component {
constructor(props) {
super(props);

this.handleSetDefault = this.handleSetDefault.bind(this);
}

handleSetDefault() {
const { method, onSetDefaultMethod } = this.props;
const { endpoints: { setDefault } } = this.context;

const token = Config.get('SecurityID');
const endpoint = `${setDefault.replace('{urlSegment}', method.urlSegment)}?SecurityID=${token}`;

fetch(endpoint, {
method: 'PUT',
}).then(response => response.json().then(json => {
if (response.status === 200) {
onSetDefaultMethod(method.urlSegment);
return;
}

const message = (json.errors && ` Errors: \n - ${json.errors.join('\n -')}`) || '';
throw Error(`Could not set default method. Error code ${response.status}.${message}`);
}));
}

render() {
const { ss: { i18n } } = window;

return (
<button
className="registered-method-list-item__control"
type="button"
onClick={this.handleSetDefault}
>
{i18n._t(
'MultiFactorAuthentication.SET_AS_DEFAULT',
fallbacks['MultiFactorAuthentication.SET_AS_DEFAULT']
)}
</button>
);
}
}

SetDefault.propTypes = {
method: registeredMethodShape.isRequired,
};

SetDefault.contextTypes = {
endpoints: PropTypes.shape({
setDefault: PropTypes.string
}),
};

const mapDispatchToProps = dispatch => ({
onSetDefaultMethod: urlSegment => dispatch(setDefaultMethod(urlSegment)),
});

export { SetDefault as Component };

export default connect(null, mapDispatchToProps)(SetDefault);
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
setAvailableMethods,
showScreen,
} from 'state/mfaRegister/actions';
import { setRegisteredMethods } from 'state/mfaAdministration/actions';
import { setDefaultMethod, setRegisteredMethods } from 'state/mfaAdministration/actions';
import {
SCREEN_CHOOSE_METHOD,
SCREEN_INTRODUCTION
Expand Down Expand Up @@ -42,12 +42,14 @@ class RegisteredMFAMethodListField extends Component {

componentDidMount() {
const {
onSetDefaultMethod, initialDefaultMethod,
onSetRegisteredMethods, initialRegisteredMethods,
onUpdateAvailableMethods, initialAvailableMethods,
} = this.props;

onSetRegisteredMethods(initialRegisteredMethods);
onUpdateAvailableMethods(initialAvailableMethods);
onSetDefaultMethod(initialDefaultMethod);
}

componentDidUpdate(prevProps, prevState) {
Expand All @@ -66,11 +68,12 @@ class RegisteredMFAMethodListField extends Component {
}

/**
* The backup and default methods are rendered separately
* The backup method is rendered separately
*
* @returns {Array<object>}
*/
getBaseMethods() {
const { backupMethod, defaultMethod } = this.props;
const { backupMethod } = this.props;
let { registeredMethods: methods } = this.props;

if (!methods) {
Expand All @@ -81,10 +84,6 @@ class RegisteredMFAMethodListField extends Component {
methods = methods.filter(method => method.urlSegment !== backupMethod.urlSegment);
}

if (defaultMethod) {
methods = methods.filter(method => method.urlSegment !== defaultMethod.urlSegment);
}

return methods;
}

Expand Down Expand Up @@ -172,7 +171,7 @@ class RegisteredMFAMethodListField extends Component {
const props = {
method,
key: method.urlSegment,
isDefaultMethod: defaultMethod && method.urlSegment === defaultMethod.urlSegment,
isDefaultMethod: defaultMethod && method.urlSegment === defaultMethod,
canRemove: !readOnly,
canReset: !readOnly,
};
Expand Down Expand Up @@ -268,8 +267,9 @@ class RegisteredMFAMethodListField extends Component {

RegisteredMFAMethodListField.propTypes = {
backupMethod: registeredMethodShape,
defaultMethod: registeredMethodShape,
defaultMethod: PropTypes.string,
readOnly: PropTypes.bool,
initialDefaultMethod: PropTypes.string,
initialRegisteredMethods: PropTypes.arrayOf(registeredMethodShape),
initialAvailableMethods: PropTypes.arrayOf(availableMethodShape),
allAvailableMethods: PropTypes.arrayOf(availableMethodShape),
Expand All @@ -296,6 +296,7 @@ RegisteredMFAMethodListField.childContextTypes = {
endpoints: PropTypes.shape({
register: PropTypes.string,
remove: PropTypes.string,
setDefault: PropTypes.string,
}),
resources: PropTypes.object,
};
Expand All @@ -308,17 +309,21 @@ const mapDispatchToProps = dispatch => ({
onUpdateAvailableMethods: methods => {
dispatch(setAvailableMethods(methods));
},
onSetDefaultMethod: urlSegment => {
dispatch(setDefaultMethod(urlSegment));
},
onSetRegisteredMethods: methods => {
dispatch(setRegisteredMethods(methods));
},
});

const mapStateToProps = state => {
const { availableMethods, screen } = state.mfaRegister;
const { registeredMethods } = state.mfaAdministration;
const { defaultMethod, registeredMethods } = state.mfaAdministration;

return {
availableMethods,
defaultMethod,
registeredMethods: registeredMethods || [],
registrationScreen: screen,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* global jest, describe, it, expect */
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import MethodListItem from '../MethodListItem';

Enzyme.configure({ adapter: new Adapter() });

window.ss = {
i18n: {
_t: (key, string) => string,
detectLocale: () => 'en_NZ',
inject: (message) => message, // not a great mock...
},
};

describe('MethodListitem', () => {
describe('getStatusMessage()', () => {
it('identifies default methods', () => {
const wrapper = shallow(
<MethodListItem
method={{ urlSegment: 'foo', }}
isDefaultMethod
/>
);

expect(wrapper.instance().getStatusMessage()).toContain('(default)');
});

it('identifies backup methods', () => {
const wrapper = shallow(
<MethodListItem
method={{ urlSegment: 'foo', }}
isBackupMethod
/>
);

expect(wrapper.instance().getStatusMessage()).toContain('Created');
});
});
});

0 comments on commit 0ab8bdc

Please sign in to comment.