Multi-Factor Auth with OTP See merge request soapbox-pub/soapbox-fe!124merge-requests/155/head
commit
92f43566e0
@ -0,0 +1,180 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const TOTP_SETTINGS_FETCH_REQUEST = 'TOTP_SETTINGS_FETCH_REQUEST';
|
||||||
|
export const TOTP_SETTINGS_FETCH_SUCCESS = 'TOTP_SETTINGS_FETCH_SUCCESS';
|
||||||
|
export const TOTP_SETTINGS_FETCH_FAIL = 'TOTP_SETTINGS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const BACKUP_CODES_FETCH_REQUEST = 'BACKUP_CODES_FETCH_REQUEST';
|
||||||
|
export const BACKUP_CODES_FETCH_SUCCESS = 'BACKUP_CODES_FETCH_SUCCESS';
|
||||||
|
export const BACKUP_CODES_FETCH_FAIL = 'BACKUP_CODES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const TOTP_SETUP_FETCH_REQUEST = 'TOTP_SETUP_FETCH_REQUEST';
|
||||||
|
export const TOTP_SETUP_FETCH_SUCCESS = 'TOTP_SETUP_FETCH_SUCCESS';
|
||||||
|
export const TOTP_SETUP_FETCH_FAIL = 'TOTP_SETUP_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const CONFIRM_TOTP_REQUEST = 'CONFIRM_TOTP_REQUEST';
|
||||||
|
export const CONFIRM_TOTP_SUCCESS = 'CONFIRM_TOTP_SUCCESS';
|
||||||
|
export const CONFIRM_TOTP_FAIL = 'CONFIRM_TOTP_FAIL';
|
||||||
|
|
||||||
|
export const DISABLE_TOTP_REQUEST = 'DISABLE_TOTP_REQUEST';
|
||||||
|
export const DISABLE_TOTP_SUCCESS = 'DISABLE_TOTP_SUCCESS';
|
||||||
|
export const DISABLE_TOTP_FAIL = 'DISABLE_TOTP_FAIL';
|
||||||
|
|
||||||
|
export function fetchUserMfaSettings() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: TOTP_SETTINGS_FETCH_REQUEST });
|
||||||
|
return api(getState).get('/api/pleroma/accounts/mfa').then(response => {
|
||||||
|
dispatch({ type: TOTP_SETTINGS_FETCH_SUCCESS, totpEnabled: response.data.totp });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: TOTP_SETTINGS_FETCH_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUserMfaSettingsRequest() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETTINGS_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchUserMfaSettingsSuccess() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETTINGS_FETCH_SUCCESS,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchUserMfaSettingsFail() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETTINGS_FETCH_FAIL,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBackupCodes() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: BACKUP_CODES_FETCH_REQUEST });
|
||||||
|
return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(response => {
|
||||||
|
dispatch({ type: BACKUP_CODES_FETCH_SUCCESS, backup_codes: response.data });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: BACKUP_CODES_FETCH_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBackupCodesRequest() {
|
||||||
|
return {
|
||||||
|
type: BACKUP_CODES_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBackupCodesSuccess(backup_codes, response) {
|
||||||
|
return {
|
||||||
|
type: BACKUP_CODES_FETCH_SUCCESS,
|
||||||
|
backup_codes: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBackupCodesFail(error) {
|
||||||
|
return {
|
||||||
|
type: BACKUP_CODES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchToptSetup() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: TOTP_SETUP_FETCH_REQUEST });
|
||||||
|
return api(getState).get('/api/pleroma/accounts/mfa/setup/totp').then(response => {
|
||||||
|
dispatch({ type: TOTP_SETUP_FETCH_SUCCESS, totp_setup: response.data });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: TOTP_SETUP_FETCH_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchToptSetupRequest() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETUP_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchToptSetupSuccess(totp_setup, response) {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETUP_FETCH_SUCCESS,
|
||||||
|
totp_setup: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchToptSetupFail(error) {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETUP_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function confirmToptSetup(code, password) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: CONFIRM_TOTP_REQUEST, code });
|
||||||
|
return api(getState).post('/api/pleroma/accounts/mfa/confirm/totp', {
|
||||||
|
code,
|
||||||
|
password,
|
||||||
|
}).then(response => {
|
||||||
|
dispatch({ type: CONFIRM_TOTP_SUCCESS });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: CONFIRM_TOTP_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmToptRequest() {
|
||||||
|
return {
|
||||||
|
type: CONFIRM_TOTP_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function confirmToptSuccess(backup_codes, response) {
|
||||||
|
return {
|
||||||
|
type: CONFIRM_TOTP_SUCCESS,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function confirmToptFail(error) {
|
||||||
|
return {
|
||||||
|
type: CONFIRM_TOTP_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableToptSetup(password) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: DISABLE_TOTP_REQUEST });
|
||||||
|
return api(getState).delete('/api/pleroma/accounts/mfa/totp', { data: { password } }).then(response => {
|
||||||
|
dispatch({ type: DISABLE_TOTP_SUCCESS });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: DISABLE_TOTP_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableToptRequest() {
|
||||||
|
return {
|
||||||
|
type: DISABLE_TOTP_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableToptSuccess(backup_codes, response) {
|
||||||
|
return {
|
||||||
|
type: DISABLE_TOTP_SUCCESS,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableToptFail(error) {
|
||||||
|
return {
|
||||||
|
type: DISABLE_TOTP_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import OtpAuthForm from '../otp_auth_form';
|
||||||
|
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
describe('<OtpAuthForm />', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
|
||||||
|
const store = mockStore(ImmutableMap({ mfa_auth_needed: true }));
|
||||||
|
|
||||||
|
const wrapper = createComponent(
|
||||||
|
<OtpAuthForm
|
||||||
|
mfa_token={'12345'}
|
||||||
|
/>,
|
||||||
|
{ store }
|
||||||
|
).toJSON();
|
||||||
|
|
||||||
|
expect(wrapper).toEqual(expect.objectContaining({
|
||||||
|
type: 'form',
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.children[0].children[0].children[0].children[0]).toEqual(expect.objectContaining({
|
||||||
|
type: 'h1',
|
||||||
|
props: { className: 'otp-login' },
|
||||||
|
children: [ 'OTP Login' ],
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { otpVerify } from 'soapbox/actions/auth';
|
||||||
|
import { fetchMe } from 'soapbox/actions/me';
|
||||||
|
import { SimpleInput } from 'soapbox/features/forms';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' },
|
||||||
|
otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect()
|
||||||
|
@injectIntl
|
||||||
|
class OtpAuthForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isLoading: false,
|
||||||
|
code_error: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
mfa_token: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
getFormData = (form) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(form).map(i => [i.name, i.value])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (event) => {
|
||||||
|
const { dispatch, mfa_token } = this.props;
|
||||||
|
const { code } = this.getFormData(event.target);
|
||||||
|
dispatch(otpVerify(code, mfa_token)).then(() => {
|
||||||
|
this.setState({ code_error: false });
|
||||||
|
return dispatch(fetchMe());
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
if (error.response.data.error === 'Invalid code') {
|
||||||
|
this.setState({ code_error: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { code_error } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='simple_form new_user otp-auth' onSubmit={this.handleSubmit}>
|
||||||
|
<fieldset disabled={this.state.isLoading}>
|
||||||
|
<div className='fields-group'>
|
||||||
|
<div className='input email optional user_email'>
|
||||||
|
<h1 className='otp-login'>
|
||||||
|
<FormattedMessage id='login.otp_log_in' defaultMessage='OTP Login' />
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className='input code optional otp_code'>
|
||||||
|
<SimpleInput
|
||||||
|
label={intl.formatMessage(messages.otpCodeLabel)}
|
||||||
|
hint={intl.formatMessage(messages.otpCodeHint)}
|
||||||
|
name='code'
|
||||||
|
type='text'
|
||||||
|
autoComplete='off'
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{ code_error &&
|
||||||
|
<div className='error-box'>
|
||||||
|
<FormattedMessage id='login.otp_log_in.fail' defaultMessage='Invalid code, please try again.' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className='actions'>
|
||||||
|
<button name='button' type='submit' className='btn button button-primary'>
|
||||||
|
<FormattedMessage id='login.log_in' defaultMessage='Log in' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,333 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import QRCode from 'qrcode.react';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
import ColumnSubheading from '../ui/components/column_subheading';
|
||||||
|
import LoadingIndicator from 'soapbox/components/loading_indicator';
|
||||||
|
import Button from 'soapbox/components/button';
|
||||||
|
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||||
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
|
import {
|
||||||
|
SimpleForm,
|
||||||
|
SimpleInput,
|
||||||
|
FieldsGroup,
|
||||||
|
TextInput,
|
||||||
|
} from 'soapbox/features/forms';
|
||||||
|
import {
|
||||||
|
fetchBackupCodes,
|
||||||
|
fetchToptSetup,
|
||||||
|
confirmToptSetup,
|
||||||
|
fetchUserMfaSettings,
|
||||||
|
disableToptSetup,
|
||||||
|
} from '../../actions/mfa';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Security settings page for user account
|
||||||
|
Routed to /auth/mfa
|
||||||
|
Includes following features:
|
||||||
|
- Set up Multi-factor Auth
|
||||||
|
*/
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.security', defaultMessage: 'Security' },
|
||||||
|
subheading: { id: 'column.mfa', defaultMessage: 'Multi-Factor Authentication' },
|
||||||
|
mfa_cancel_button: { id: 'column.mfa_cancel', defaultMessage: 'Cancel' },
|
||||||
|
mfa_setup_button: { id: 'column.mfa_setup', defaultMessage: 'Proceed to Setup' },
|
||||||
|
mfa_setup_confirm_button: { id: 'column.mfa_confirm_button', defaultMessage: 'Confirm' },
|
||||||
|
mfa_setup_disable_button: { id: 'column.mfa_disable_button', defaultMessage: 'Disable' },
|
||||||
|
passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' },
|
||||||
|
confirmFail: { id: 'security.confirm.fail', defaultMessage: 'Incorrect code or password. Try again.' },
|
||||||
|
qrFail: { id: 'security.qr.fail', defaultMessage: 'Failed to fetch setup key' },
|
||||||
|
codesFail: { id: 'security.codes.fail', defaultMessage: 'Failed to fetch backup codes' },
|
||||||
|
disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
backup_codes: state.getIn(['auth', 'backup_codes', 'codes']),
|
||||||
|
settings: getSettings(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class MfaForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.props.dispatch(fetchUserMfaSettings()).then(response => {
|
||||||
|
this.props.dispatch(changeSetting(['otpEnabled'], response.data.settings.enabled));
|
||||||
|
// this.setState({ otpEnabled: response.data.settings.enabled });
|
||||||
|
}).catch(e => e);
|
||||||
|
this.handleSetupProceedClick = this.handleSetupProceedClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
displayOtpForm: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSetupProceedClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({ displayOtpForm: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, settings } = this.props;
|
||||||
|
const { displayOtpForm } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column icon='lock' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
|
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||||
|
{ settings.get('otpEnabled') === true && <DisableOtpForm />}
|
||||||
|
{ settings.get('otpEnabled') === false && <EnableOtpForm handleSetupProceedClick={this.handleSetupProceedClick} />}
|
||||||
|
{ settings.get('otpEnabled') === false && displayOtpForm && <OtpConfirmForm /> }
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
class DisableOtpForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOtpDisableClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { password } = this.state;
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(disableToptSetup(password)).then(response => {
|
||||||
|
this.context.router.history.push('../auth/edit');
|
||||||
|
dispatch(changeSetting(['otpEnabled'], false));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.disableFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='security-settings-panel'>
|
||||||
|
<h1 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.otp_enabled_title' defaultMessage='OTP Enabled' />
|
||||||
|
</h1>
|
||||||
|
<div><FormattedMessage id='mfa.otp_enabled_description' defaultMessage='You have enabled two-factor authentication via OTP.' /></div>
|
||||||
|
<div><FormattedMessage id='mfa.mfa_disable_enter_password' defaultMessage='Enter your current password to disable two-factor auth:' /></div>
|
||||||
|
<SimpleInput
|
||||||
|
type='password'
|
||||||
|
name='password'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
|
<Button className='button button-primary disable' text={intl.formatMessage(messages.mfa_setup_disable_button)} onClick={this.handleOtpDisableClick} />
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
class EnableOtpForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
backupCodes: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(fetchBackupCodes()).then(response => {
|
||||||
|
this.setState({ backupCodes: response.data.codes });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.codesFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelClick = e => {
|
||||||
|
this.context.router.history.push('../auth/edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { backupCodes, displayOtpForm } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='security-settings-panel'>
|
||||||
|
<h1 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.setup_otp_title' defaultMessage='OTP Disabled' />
|
||||||
|
</h1>
|
||||||
|
<h2 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.setup_hint' defaultMessage='Follow these steps to set up multi-factor authentication on your account with OTP' />
|
||||||
|
</h2>
|
||||||
|
<div className='security-warning'>
|
||||||
|
<FormattedMessage id='mfa.setup_warning' defaultMessage="Write these codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account." />
|
||||||
|
</div>
|
||||||
|
<h2 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.setup_recoverycodes' defaultMessage='Recovery codes' />
|
||||||
|
</h2>
|
||||||
|
<div className='backup_codes'>
|
||||||
|
{ backupCodes.length ?
|
||||||
|
<div>
|
||||||
|
{backupCodes.map((code, i) => (
|
||||||
|
<div key={i} className='backup_code'>
|
||||||
|
<div className='backup_code'>{code}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div> :
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{ !displayOtpForm &&
|
||||||
|
<div className='security-settings-panel__setup-otp__buttons'>
|
||||||
|
<Button className='button button-secondary cancel' text={intl.formatMessage(messages.mfa_cancel_button)} onClick={this.handleCancelClick} />
|
||||||
|
{ backupCodes.length ?
|
||||||
|
<Button className='button button-primary setup' text={intl.formatMessage(messages.mfa_setup_button)} onClick={this.props.handleSetupProceedClick} /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
class OtpConfirmForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
password: '',
|
||||||
|
done: false,
|
||||||
|
code: '',
|
||||||
|
qrCodeURI: '',
|
||||||
|
confirm_key: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(fetchToptSetup()).then(response => {
|
||||||
|
this.setState({ qrCodeURI: response.data.provisioning_uri, confirm_key: response.data.key });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.qrFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOtpConfirmClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { code, password } = this.state;
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(confirmToptSetup(code, password)).then(response => {
|
||||||
|
dispatch(changeSetting(['otpEnabled'], true));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.confirmFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { qrCodeURI, confirm_key } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='security-settings-panel'>
|
||||||
|
|
||||||
|
<fieldset disabled={false}>
|
||||||
|
<FieldsGroup>
|
||||||
|
<div className='security-settings-panel__section-container'>
|
||||||
|
<h2><FormattedMessage id='mfa.mfa_setup_scan_title' defaultMessage='Scan' /></h2>
|
||||||
|
|
||||||
|
<div><FormattedMessage id='mfa.mfa_setup_scan_description' defaultMessage='Using your two-factor app, scan this QR code or enter text key:' /></div>
|
||||||
|
|
||||||
|
<span className='security-settings-panel qr-code'>
|
||||||
|
<QRCode value={qrCodeURI} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className='security-settings-panel confirm-key'><FormattedMessage id='mfa.mfa_setup_scan_key' defaultMessage='Key:' /> {confirm_key}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='security-settings-panel__section-container'>
|
||||||
|
<h2><FormattedMessage id='mfa.mfa_setup_verify_title' defaultMessage='Verify' /></h2>
|
||||||
|
|
||||||
|
<div><FormattedMessage id='mfa.mfa_setup_verify_description' defaultMessage='To enable two-factor authentication, enter the code from your two-factor app:' /></div>
|
||||||
|
<TextInput
|
||||||
|
name='code'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div><FormattedMessage id='mfa.mfa_setup_enter_password' defaultMessage='Enter your current password to confirm your identity:' /></div>
|
||||||
|
<SimpleInput
|
||||||
|
type='password'
|
||||||
|
name='password'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FieldsGroup>
|
||||||
|
</fieldset>
|
||||||
|
<div className='security-settings-panel__setup-otp__buttons'>
|
||||||
|
<Button className='button button-secondary cancel' text={intl.formatMessage(messages.mfa_cancel_button)} onClick={this.handleCancelClick} />
|
||||||
|
<Button className='button button-primary setup' text={intl.formatMessage(messages.mfa_setup_confirm_button)} onClick={this.handleOtpConfirmClick} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
.security-settings-panel {
|
||||||
|
margin: 20px;
|
||||||
|
|
||||||
|
h1.security-settings-panel__setup-otp {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.security-settings-panel__setup-otp {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-warning {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--warning-color--faint);
|
||||||
|
margin: 5px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup_codes {
|
||||||
|
margin: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--brand-color--faint);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 125px;
|
||||||
|
|
||||||
|
.backup_code {
|
||||||
|
margin: 5px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-settings-panel__setup-otp__buttons {
|
||||||
|
margin: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-width: 182px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.confirm-key {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0 0 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.otp-auth {
|
||||||
|
.error-box {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: $error-red;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue