mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-25 14:39:53 +05:30
183 lines
5.1 KiB
TypeScript
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;
|
|
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 }));
|
|
};
|
|
}
|