mirror of
				https://github.com/elyby/accounts-frontend.git
				synced 2025-05-31 14:11:58 +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 }));
 | 
						|
    };
 | 
						|
}
 |