2017-09-09 19:52:19 +05:30
|
|
|
// @flow
|
|
|
|
import React, { Component } from 'react';
|
2019-01-28 00:42:58 +05:30
|
|
|
import PropTypes from 'prop-types';
|
2017-09-09 19:52:19 +05:30
|
|
|
|
|
|
|
import { Button, FormModel } from 'components/ui/form';
|
|
|
|
import styles from 'components/profile/profileForm.scss';
|
|
|
|
import Stepper from 'components/ui/stepper';
|
2017-11-19 23:46:15 +05:30
|
|
|
import { SlideMotion } from 'components/ui/motion';
|
2017-10-28 19:08:07 +05:30
|
|
|
import { ScrollIntoView } from 'components/ui/scroll';
|
2017-09-09 19:52:19 +05:30
|
|
|
import logger from 'services/logger';
|
2019-01-28 00:42:58 +05:30
|
|
|
import { getSecret, enable as enableMFA } from 'services/api/mfa';
|
2017-09-09 19:52:19 +05:30
|
|
|
|
|
|
|
import Instructions from './instructions';
|
|
|
|
import KeyForm from './keyForm';
|
|
|
|
import Confirmation from './confirmation';
|
|
|
|
import messages from './MultiFactorAuth.intl.json';
|
|
|
|
|
|
|
|
import type { Form } from 'components/ui/form';
|
|
|
|
|
|
|
|
const STEPS_TOTAL = 3;
|
|
|
|
|
2017-09-09 20:34:26 +05:30
|
|
|
export type MfaStep = 0 | 1 | 2;
|
2017-09-09 19:52:19 +05:30
|
|
|
type Props = {
|
2019-11-27 14:33:32 +05:30
|
|
|
onChangeStep: Function,
|
|
|
|
confirmationForm: FormModel,
|
|
|
|
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
|
|
|
onComplete: Function,
|
|
|
|
step: MfaStep,
|
2017-09-09 19:52:19 +05:30
|
|
|
};
|
|
|
|
|
2019-01-28 00:42:58 +05:30
|
|
|
interface State {
|
2019-11-27 14:33:32 +05:30
|
|
|
isLoading: boolean;
|
|
|
|
activeStep: MfaStep;
|
|
|
|
secret: string;
|
|
|
|
qrCodeSrc: string;
|
2019-01-28 00:42:58 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
export default class MfaEnable extends Component<Props, State> {
|
2019-11-27 14:33:32 +05:30
|
|
|
static defaultProps = {
|
|
|
|
confirmationForm: new FormModel(),
|
|
|
|
step: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
static contextTypes = {
|
|
|
|
userId: PropTypes.number.isRequired,
|
|
|
|
};
|
|
|
|
|
|
|
|
state = {
|
|
|
|
isLoading: false,
|
|
|
|
activeStep: this.props.step,
|
|
|
|
qrCodeSrc: '',
|
|
|
|
secret: '',
|
|
|
|
};
|
|
|
|
|
|
|
|
confirmationFormEl: ?Form;
|
|
|
|
|
|
|
|
componentWillMount() {
|
|
|
|
this.syncState(this.props);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillReceiveProps(nextProps: Props) {
|
|
|
|
this.syncState(nextProps);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { activeStep, isLoading } = this.state;
|
|
|
|
|
|
|
|
const stepsData = [
|
|
|
|
{
|
|
|
|
buttonLabel: messages.theAppIsInstalled,
|
|
|
|
buttonAction: () => this.nextStep(),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
buttonLabel: messages.ready,
|
|
|
|
buttonAction: () => this.nextStep(),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
buttonLabel: messages.enable,
|
|
|
|
buttonAction: () =>
|
|
|
|
this.confirmationFormEl && this.confirmationFormEl.submit(),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
const { buttonLabel, buttonAction } = stepsData[activeStep];
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div className={styles.stepper}>
|
|
|
|
<Stepper totalSteps={STEPS_TOTAL} activeStep={activeStep} />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className={styles.form}>
|
|
|
|
{activeStep > 0 ? <ScrollIntoView /> : null}
|
|
|
|
|
|
|
|
{this.renderStepForms()}
|
|
|
|
|
|
|
|
<Button
|
|
|
|
color="green"
|
|
|
|
onClick={buttonAction}
|
|
|
|
loading={isLoading}
|
|
|
|
block
|
|
|
|
label={buttonLabel}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderStepForms() {
|
|
|
|
const { activeStep, secret, qrCodeSrc } = this.state;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SlideMotion activeStep={activeStep}>
|
|
|
|
{[
|
|
|
|
<Instructions key="step1" />,
|
|
|
|
<KeyForm key="step2" secret={secret} qrCodeSrc={qrCodeSrc} />,
|
|
|
|
<Confirmation
|
|
|
|
key="step3"
|
|
|
|
form={this.props.confirmationForm}
|
|
|
|
formRef={(el: Form) => (this.confirmationFormEl = el)}
|
|
|
|
onSubmit={this.onTotpSubmit}
|
|
|
|
onInvalid={() => this.forceUpdate()}
|
|
|
|
/>,
|
|
|
|
]}
|
|
|
|
</SlideMotion>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
syncState(props: Props) {
|
|
|
|
if (props.step === 1) {
|
|
|
|
this.setState({ isLoading: true });
|
|
|
|
|
|
|
|
getSecret(this.context.userId).then(resp => {
|
|
|
|
this.setState({
|
|
|
|
isLoading: false,
|
|
|
|
secret: resp.secret,
|
|
|
|
qrCodeSrc: resp.qr,
|
|
|
|
});
|
|
|
|
});
|
2017-09-09 19:52:19 +05:30
|
|
|
}
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
this.setState({
|
|
|
|
activeStep:
|
|
|
|
typeof props.step === 'number' ? props.step : this.state.activeStep,
|
|
|
|
});
|
|
|
|
}
|
2017-09-09 19:52:19 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
nextStep() {
|
|
|
|
const nextStep = this.state.activeStep + 1;
|
2017-09-09 19:52:19 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (nextStep < STEPS_TOTAL) {
|
|
|
|
this.props.onChangeStep(nextStep);
|
2017-09-09 19:52:19 +05:30
|
|
|
}
|
2019-11-27 14:33:32 +05:30
|
|
|
}
|
2017-09-09 19:52:19 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
onTotpSubmit = (form: FormModel): Promise<*> => {
|
|
|
|
this.setState({ isLoading: true });
|
2017-09-09 19:52:19 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return this.props
|
|
|
|
.onSubmit(form, () => {
|
|
|
|
const data = form.serialize();
|
2017-09-09 19:52:19 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return enableMFA(this.context.userId, data.totp, data.password);
|
|
|
|
})
|
|
|
|
.then(() => this.props.onComplete())
|
|
|
|
.catch(resp => {
|
|
|
|
const { errors } = resp || {};
|
2017-09-09 19:52:19 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (errors) {
|
|
|
|
return Promise.reject(errors);
|
2017-09-09 19:52:19 +05:30
|
|
|
}
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
logger.error('MFA: Unexpected form submit result', {
|
|
|
|
resp,
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.finally(() => this.setState({ isLoading: false }));
|
|
|
|
};
|
2017-09-09 19:52:19 +05:30
|
|
|
}
|