accounts-frontend/packages/app/components/profile/multiFactorAuth/MfaEnable.tsx

183 lines
5.1 KiB
TypeScript

import React from 'react';
import { FormattedMessage as Message, defineMessages } from 'react-intl';
import { Button, FormModel } from 'app/components/ui/form';
import styles from 'app/components/profile/profileForm.scss';
import Stepper from 'app/components/ui/stepper';
import { SlideMotion } from 'app/components/ui/motion';
import { ScrollIntoView } from 'app/components/ui/scroll';
import logger from 'app/services/logger';
import { getSecret, enable as enableMFA } from 'app/services/api/mfa';
import { Form } from 'app/components/ui/form';
import Context from '../Context';
import Instructions from './instructions';
import KeyForm from './keyForm';
import Confirmation from './Confirmation';
const STEPS_TOTAL = 3;
export type MfaStep = 0 | 1 | 2;
type Props = {
onChangeStep: (nextStep: number) => void;
confirmationForm: FormModel;
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
onComplete: () => void;
step: MfaStep;
};
const labels = defineMessages({
theAppIsInstalled: 'App has been installed',
ready: 'Ready',
enable: 'Enable',
});
interface State {
isLoading: boolean;
activeStep: MfaStep;
secret: string;
qrCodeSrc: string;
}
export default class MfaEnable extends React.PureComponent<Props, State> {
static contextType = Context;
/* TODO: use declare */ context: React.ContextType<typeof Context>;
static defaultProps = {
confirmationForm: new FormModel(),
step: 0,
};
state = {
isLoading: false,
activeStep: this.props.step,
qrCodeSrc: '',
secret: '',
};
confirmationFormEl: Form | null;
componentDidMount() {
this.syncState(this.props);
}
static getDerivedStateFromProps(props: Props, state: State) {
if (props.step !== state.activeStep) {
return {
activeStep: props.step,
};
}
return null;
}
componentDidUpdate() {
this.syncState(this.props);
}
render() {
const { activeStep, isLoading } = this.state;
const stepsData = [
{
buttonLabel: labels.theAppIsInstalled,
buttonAction: () => this.nextStep(),
},
{
buttonLabel: labels.ready,
buttonAction: () => this.nextStep(),
},
{
buttonLabel: labels.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>
<Message {...buttonLabel} />
</Button>
</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) => (this.confirmationFormEl = el)}
onSubmit={this.onTotpSubmit}
onInvalid={() => this.forceUpdate()}
/>
</SlideMotion>
);
}
syncState(props: Props) {
const { isLoading, qrCodeSrc } = this.state;
if (props.step === 1 && !isLoading && !qrCodeSrc) {
this.setState({ isLoading: true });
getSecret(this.context.userId).then((resp) => {
this.setState({
isLoading: false,
secret: resp.secret,
qrCodeSrc: resp.qr,
});
});
}
}
nextStep() {
const nextStep = this.state.activeStep + 1;
if (nextStep < STEPS_TOTAL) {
this.props.onChangeStep(nextStep);
}
}
onTotpSubmit = (form: FormModel): Promise<void> => {
this.setState({ isLoading: true });
return this.props
.onSubmit(form, () => {
const data = form.serialize();
return enableMFA(this.context.userId, data.totp, data.password);
})
.then(() => this.props.onComplete())
.catch((resp) => {
const { errors } = resp || {};
if (errors) {
return Promise.reject(errors);
}
logger.error('MFA: Unexpected form submit result', {
resp,
});
})
.finally(() => this.setState({ isLoading: false }));
};
}