mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-03-06 15:59:16 +05:30
#305: fully connect mfa ui with backend
This commit is contained in:
parent
b98be6b737
commit
42399ef9bf
@ -7,6 +7,7 @@
|
||||
"projectRules": "project rules",
|
||||
"changedAt": "Changed {at}",
|
||||
"disabled": "Disabled",
|
||||
"enabled": "Enabled",
|
||||
"nickname": "Nickname",
|
||||
"password": "Password",
|
||||
"twoFactorAuth": "Two factor auth"
|
||||
|
@ -91,8 +91,13 @@ class Profile extends Component {
|
||||
/>
|
||||
|
||||
<ProfileField
|
||||
link="/profile/mfa"
|
||||
label={<Message {...messages.twoFactorAuth} />}
|
||||
value={<Message {...messages.disabled} />}
|
||||
value={user.isOtpEnabled ? (
|
||||
<Message {...messages.enabled} />
|
||||
) : (
|
||||
<Message {...messages.disabled} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<ProfileField
|
||||
|
@ -4,12 +4,12 @@ import React, { Component } from 'react';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { Button, Form, FormModel } from 'components/ui/form';
|
||||
import { Button, FormModel } from 'components/ui/form';
|
||||
import { BackButton } from 'components/profile/ProfileForm';
|
||||
import styles from 'components/profile/profileForm.scss';
|
||||
import helpLinks from 'components/auth/helpLinks.scss';
|
||||
import Stepper from 'components/ui/stepper';
|
||||
import { ScrollMotion } from 'components/ui/motion';
|
||||
import logger from 'services/logger';
|
||||
import mfa from 'services/api/mfa';
|
||||
|
||||
import Instructions from './instructions';
|
||||
@ -17,43 +17,43 @@ 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;
|
||||
|
||||
export type MfaStep = 0|1|2;
|
||||
type Props = {
|
||||
onChangeStep: Function,
|
||||
lang: string,
|
||||
email: string,
|
||||
stepForm: FormModel,
|
||||
onSubmit: Function,
|
||||
step: 0|1|2,
|
||||
code: string
|
||||
confirmationForm: FormModel,
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
||||
onComplete: Function,
|
||||
step: MfaStep
|
||||
};
|
||||
|
||||
export default class MultiFactorAuth extends Component {
|
||||
props: Props;
|
||||
|
||||
static defaultProps = {
|
||||
stepForm: new FormModel(),
|
||||
onChangeStep() {},
|
||||
confirmationForm: new FormModel(),
|
||||
step: 0
|
||||
};
|
||||
|
||||
state: {
|
||||
isLoading: bool,
|
||||
activeStep: number,
|
||||
activeStep: MfaStep,
|
||||
secret: string,
|
||||
qrCodeSrc: string,
|
||||
code: string,
|
||||
newEmail: ?string
|
||||
qrCodeSrc: string
|
||||
} = {
|
||||
isLoading: false,
|
||||
activeStep: this.props.step,
|
||||
qrCodeSrc: '',
|
||||
secret: '',
|
||||
code: this.props.code || '',
|
||||
newEmail: null
|
||||
secret: ''
|
||||
};
|
||||
|
||||
confirmationFormEl: ?Form;
|
||||
|
||||
componentWillMount() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
@ -64,100 +64,84 @@ export default class MultiFactorAuth extends Component {
|
||||
|
||||
render() {
|
||||
const {activeStep, isLoading} = this.state;
|
||||
const form = this.props.stepForm;
|
||||
|
||||
const stepsData = [
|
||||
{
|
||||
buttonLabel: messages.theAppIsInstalled
|
||||
buttonLabel: messages.theAppIsInstalled,
|
||||
buttonAction: () => this.nextStep()
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.ready
|
||||
buttonLabel: messages.ready,
|
||||
buttonAction: () => this.nextStep()
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.enableTwoFactorAuth
|
||||
buttonLabel: messages.enableTwoFactorAuth,
|
||||
buttonAction: () => this.confirmationFormEl && this.confirmationFormEl.submit()
|
||||
}
|
||||
];
|
||||
|
||||
const buttonLabel = stepsData[activeStep].buttonLabel;
|
||||
const {buttonLabel, buttonAction} = stepsData[activeStep];
|
||||
|
||||
return (
|
||||
<Form form={form}
|
||||
onSubmit={this.onFormSubmit}
|
||||
isLoading={isLoading}
|
||||
onInvalid={() => this.forceUpdate()}
|
||||
>
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<BackButton />
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<BackButton />
|
||||
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...messages.mfaTitle}>
|
||||
{(pageTitle) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...messages.mfaTitle}>
|
||||
{(pageTitle) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaDescription} />
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaDescription} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.stepper}>
|
||||
<Stepper totalSteps={STEPS_TOTAL} activeStep={activeStep} />
|
||||
</div>
|
||||
|
||||
<div className={styles.form}>
|
||||
{this.renderStepForms()}
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
type="submit"
|
||||
block
|
||||
label={buttonLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{this.isLastStep() || 1 ? null : (
|
||||
<div className={helpLinks.helpLinks}>
|
||||
<a href="#" onClick={this.onSwitchStep}>
|
||||
<Message {...messages.alreadyReceivedCode} />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className={styles.stepper}>
|
||||
<Stepper totalSteps={STEPS_TOTAL} activeStep={activeStep} />
|
||||
</div>
|
||||
|
||||
<div className={styles.form}>
|
||||
{this.renderStepForms()}
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={buttonAction}
|
||||
loading={isLoading}
|
||||
block
|
||||
label={buttonLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStepForms() {
|
||||
const {activeStep, secret, qrCodeSrc} = this.state;
|
||||
|
||||
const steps = [
|
||||
() => <Instructions key="step1" />,
|
||||
() => (
|
||||
<KeyForm key="step2"
|
||||
secret={secret}
|
||||
qrCodeSrc={qrCodeSrc}
|
||||
/>
|
||||
),
|
||||
() => (
|
||||
<Confirmation key="step3"
|
||||
form={this.props.stepForm}
|
||||
isActiveStep={activeStep === 2}
|
||||
onCodeInput={this.onCodeInput}
|
||||
/>
|
||||
)
|
||||
];
|
||||
|
||||
return (
|
||||
<ScrollMotion activeStep={activeStep}>
|
||||
{steps.map((renderStep) => renderStep())}
|
||||
{[
|
||||
<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()}
|
||||
/>
|
||||
]}
|
||||
</ScrollMotion>
|
||||
);
|
||||
}
|
||||
@ -165,72 +149,53 @@ export default class MultiFactorAuth extends Component {
|
||||
syncState(props: Props) {
|
||||
if (props.step === 1) {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
mfa.getSecret().then((resp) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
activeStep: props.step,
|
||||
secret: resp.secret,
|
||||
qrCodeSrc: resp.qr
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
activeStep: typeof props.step === 'number' ? props.step : this.state.activeStep,
|
||||
code: props.code || ''
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
activeStep: typeof props.step === 'number' ? props.step : this.state.activeStep
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
const {activeStep} = this.state;
|
||||
const nextStep = activeStep + 1;
|
||||
const newEmail = null;
|
||||
const nextStep = this.state.activeStep + 1;
|
||||
|
||||
if (nextStep < STEPS_TOTAL) {
|
||||
this.setState({
|
||||
activeStep: nextStep,
|
||||
newEmail
|
||||
});
|
||||
|
||||
this.props.onChangeStep(nextStep);
|
||||
} else {
|
||||
this.props.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
isLastStep() {
|
||||
return this.state.activeStep + 1 === STEPS_TOTAL;
|
||||
}
|
||||
onTotpSubmit = (form: FormModel): Promise<*> => {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
onSwitchStep = (event: Event) => {
|
||||
event.preventDefault();
|
||||
return this.props.onSubmit(
|
||||
form,
|
||||
() => {
|
||||
const data = form.serialize();
|
||||
|
||||
this.nextStep();
|
||||
};
|
||||
return mfa.enable(data);
|
||||
}
|
||||
)
|
||||
.catch((resp) => {
|
||||
const {errors} = resp || {};
|
||||
|
||||
onCodeInput = (event: {target: HTMLInputElement}) => {
|
||||
const {value} = event.target;
|
||||
if (errors) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
code: this.props.code || value
|
||||
});
|
||||
};
|
||||
|
||||
onFormSubmit = () => {
|
||||
this.nextStep();
|
||||
// const {activeStep} = this.state;
|
||||
// const form = this.props.stepForms[activeStep];
|
||||
// const promise = this.props.onSubmit(activeStep, form);
|
||||
//
|
||||
// if (!promise || !promise.then) {
|
||||
// throw new Error('Expecting promise from onSubmit');
|
||||
// }
|
||||
//
|
||||
// promise.then(() => this.nextStep(), (resp) => {
|
||||
// if (resp.errors) {
|
||||
// form.setErrors(resp.errors);
|
||||
// this.forceUpdate();
|
||||
// } else {
|
||||
// return Promise.reject(resp);
|
||||
// }
|
||||
// });
|
||||
logger.error('MFA: Unexpected form submit result', {
|
||||
resp
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({isLoading: false}));
|
||||
};
|
||||
}
|
||||
|
@ -3,38 +3,44 @@ import React from 'react';
|
||||
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { Input, FormModel } from 'components/ui/form';
|
||||
import { Input, Form, FormModel } from 'components/ui/form';
|
||||
|
||||
import profileForm from 'components/profile/profileForm.scss';
|
||||
import messages from '../MultiFactorAuth.intl.json';
|
||||
|
||||
export default function Confirmation({
|
||||
form,
|
||||
isActiveStep,
|
||||
onCodeInput
|
||||
formRef = () => {},
|
||||
onSubmit,
|
||||
onInvalid
|
||||
}: {
|
||||
form: FormModel,
|
||||
isActiveStep: bool,
|
||||
onCodeInput: (event: Event & {target: HTMLInputElement}) => void
|
||||
formRef?: (el: ?Form) => void,
|
||||
onSubmit: () => Promise<*>,
|
||||
onInvalid: Function
|
||||
}) {
|
||||
return (
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.enterCodeFromApp} />
|
||||
</p>
|
||||
</div>
|
||||
<Form form={form}
|
||||
onSubmit={onSubmit}
|
||||
onInvalid={onInvalid}
|
||||
ref={formRef}
|
||||
>
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.enterCodeFromApp} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<Input {...form.bindField('key')}
|
||||
required={isActiveStep}
|
||||
onChange={onCodeInput}
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
color="violet"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
<div className={profileForm.formRow}>
|
||||
<Input {...form.bindField('totp')}
|
||||
required
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './MultiFactorAuth';
|
||||
export { default, MfaStep } from './MultiFactorAuth';
|
||||
|
@ -79,3 +79,13 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
background: url('./form/images/loader_button.gif') #95a5a6 center center !important;
|
||||
|
||||
cursor: default;
|
||||
color: #fff;
|
||||
transition: 0.25s;
|
||||
outline: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -1,27 +1,23 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import buttons from 'components/ui/buttons.scss';
|
||||
import { colors, COLOR_GREEN } from 'components/ui';
|
||||
import { omit } from 'functions';
|
||||
import { COLOR_GREEN } from 'components/ui';
|
||||
|
||||
import FormComponent from './FormComponent';
|
||||
|
||||
export default class Button extends FormComponent {
|
||||
static displayName = 'Button';
|
||||
import type { Color } from 'components/ui';
|
||||
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
}),
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
block: PropTypes.bool,
|
||||
small: PropTypes.bool,
|
||||
color: PropTypes.oneOf(colors),
|
||||
className: PropTypes.string
|
||||
export default class Button extends FormComponent {
|
||||
props: {
|
||||
label: string | {id: string},
|
||||
block: bool,
|
||||
small: bool,
|
||||
loading: bool,
|
||||
className: string,
|
||||
color: Color
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -29,20 +25,25 @@ export default class Button extends FormComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { color, block, small, className } = this.props;
|
||||
|
||||
const props = omit(this.props, Object.keys(Button.propTypes));
|
||||
|
||||
const label = this.formatMessage(this.props.label);
|
||||
const {
|
||||
color,
|
||||
block,
|
||||
small,
|
||||
className,
|
||||
loading,
|
||||
label,
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<button className={classNames(buttons[color], {
|
||||
[buttons.loading]: loading,
|
||||
[buttons.block]: block,
|
||||
[buttons.smallButton]: small
|
||||
}, className)}
|
||||
{...props}
|
||||
{...restProps}
|
||||
>
|
||||
{label}
|
||||
{this.formatMessage(label)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import logger from 'services/logger';
|
||||
|
||||
import FormModel from './FormModel';
|
||||
import styles from './form.scss';
|
||||
|
||||
export default class Form extends Component {
|
||||
static displayName = 'Form';
|
||||
import type FormModel from './FormModel';
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string, // and id, that uniquely identifies form contents
|
||||
isLoading: PropTypes.bool,
|
||||
form: PropTypes.instanceOf(FormModel),
|
||||
onSubmit: PropTypes.func,
|
||||
onInvalid: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
])
|
||||
};
|
||||
type Props = {
|
||||
id: string,
|
||||
isLoading: bool,
|
||||
form?: FormModel,
|
||||
onSubmit: Function,
|
||||
onInvalid: (errors: {[errorKey: string]: string}) => void,
|
||||
children: *
|
||||
};
|
||||
type InputElement = HTMLInputElement|HTMLTextAreaElement;
|
||||
|
||||
export default class Form extends Component {
|
||||
props: Props;
|
||||
|
||||
static defaultProps = {
|
||||
id: 'default',
|
||||
@ -34,13 +34,15 @@ export default class Form extends Component {
|
||||
isLoading: this.props.isLoading || false
|
||||
};
|
||||
|
||||
formEl: ?HTMLFormElement;
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.form) {
|
||||
this.props.form.addLoadingListener(this.onLoading);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.id !== this.props.id) {
|
||||
this.setState({
|
||||
isTouched: false
|
||||
@ -55,9 +57,13 @@ export default class Form extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (nextProps.form && this.props.form && nextProps.form !== this.props.form) {
|
||||
const nextForm = nextProps.form;
|
||||
if (nextForm
|
||||
&& this.props.form
|
||||
&& nextForm !== this.props.form
|
||||
) {
|
||||
this.props.form.removeLoadingListener(this.onLoading);
|
||||
nextProps.form.addLoadingListener(this.onLoading);
|
||||
nextForm.addLoadingListener(this.onLoading);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +86,7 @@ export default class Form extends Component {
|
||||
}
|
||||
)}
|
||||
onSubmit={this.onFormSubmit}
|
||||
ref={(el: ?HTMLFormElement) => this.formEl = el}
|
||||
noValidate
|
||||
>
|
||||
{this.props.children}
|
||||
@ -87,25 +94,32 @@ export default class Form extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onFormSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
submit() {
|
||||
if (!this.state.isTouched) {
|
||||
this.setState({
|
||||
isTouched: true
|
||||
});
|
||||
}
|
||||
|
||||
const form = event.currentTarget;
|
||||
const form = this.formEl;
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.checkValidity()) {
|
||||
this.props.onSubmit();
|
||||
Promise.resolve(this.props.onSubmit(
|
||||
this.props.form ? this.props.form : new FormData(form)
|
||||
))
|
||||
.catch((errors: {[key: string]: string}) => {
|
||||
this.setErrors(errors);
|
||||
});
|
||||
} else {
|
||||
const invalidEls = form.querySelectorAll(':invalid');
|
||||
const errors = {};
|
||||
invalidEls[0].focus(); // focus on first error
|
||||
|
||||
Array.from(invalidEls).reduce((errors, el) => {
|
||||
Array.from(invalidEls).reduce((errors, el: InputElement) => {
|
||||
if (!el.name) {
|
||||
logger.warn('Found an element without name', {el});
|
||||
|
||||
@ -124,10 +138,20 @@ export default class Form extends Component {
|
||||
return errors;
|
||||
}, errors);
|
||||
|
||||
this.props.form && this.props.form.setErrors(errors);
|
||||
this.props.onInvalid(errors);
|
||||
this.setErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(errors: {[key: string]: string}) {
|
||||
this.props.form && this.props.form.setErrors(errors);
|
||||
this.props.onInvalid(errors);
|
||||
}
|
||||
|
||||
onFormSubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
this.submit();
|
||||
};
|
||||
|
||||
onLoading = (isLoading) => this.setState({isLoading});
|
||||
onLoading = (isLoading: bool) => this.setState({isLoading});
|
||||
}
|
||||
|
@ -284,7 +284,8 @@
|
||||
}
|
||||
|
||||
[type="submit"] {
|
||||
background: url('./images/loader_button.gif') #95a5a6 center center;
|
||||
// TODO: duplicate of .loading from components/ui/buttons
|
||||
background: url('./images/loader_button.gif') #95a5a6 center center !important;
|
||||
|
||||
cursor: default;
|
||||
color: #fff;
|
||||
|
@ -11,13 +11,13 @@ export type User = {|
|
||||
lang: string,
|
||||
isGuest: bool,
|
||||
isActive: bool,
|
||||
isOtpEnabled: bool,
|
||||
passwordChangedAt: ?number,
|
||||
hasMojangUsernameCollision: bool,
|
||||
maskedEmail?: string,
|
||||
shouldAcceptRules?: bool,
|
||||
|};
|
||||
|
||||
|
||||
const defaults: User = {
|
||||
id: null,
|
||||
uuid: null,
|
||||
@ -30,6 +30,7 @@ const defaults: User = {
|
||||
avatar: '',
|
||||
lang: '',
|
||||
isActive: false,
|
||||
isOtpEnabled: false,
|
||||
shouldAcceptRules: false, // whether user need to review updated rules
|
||||
passwordChangedAt: null,
|
||||
hasMojangUsernameCollision: false,
|
||||
|
@ -1,22 +1,21 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import MultiFactorAuth from 'components/profile/multiFactorAuth';
|
||||
import MultiFactorAuth, { MfaStep } from 'components/profile/multiFactorAuth';
|
||||
|
||||
import accounts from 'services/api/accounts';
|
||||
import type { FormModel } from 'components/ui/form';
|
||||
|
||||
class MultiFactorAuthPage extends Component {
|
||||
static propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
lang: PropTypes.string.isRequired,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func
|
||||
}).isRequired,
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
step: PropTypes.oneOf(['1', '2', '3'])
|
||||
})
|
||||
})
|
||||
props: {
|
||||
history: {
|
||||
push: (string) => void
|
||||
},
|
||||
match: {
|
||||
params: {
|
||||
step?: '1'|'2'|'3'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@ -34,71 +33,32 @@ class MultiFactorAuthPage extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {step = '1'} = this.props.match.params;
|
||||
const step = (parseInt(this.props.match.params.step, 10) || 1) - 1;
|
||||
|
||||
return (
|
||||
<MultiFactorAuth
|
||||
onSubmit={this.onSubmit}
|
||||
email={this.props.email}
|
||||
lang={this.props.lang}
|
||||
step={step * 1 - 1}
|
||||
step={step}
|
||||
onChangeStep={this.onChangeStep}
|
||||
onComplete={this.onComplete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onChangeStep = (step) => {
|
||||
this.props.history.push(`/profile/mfa/step${++step}`);
|
||||
onChangeStep = (step: MfaStep) => {
|
||||
this.props.history.push(`/profile/mfa/step${step + 1}`);
|
||||
};
|
||||
|
||||
onSubmit = (step, form) => {
|
||||
onSubmit = (form: FormModel, sendData: () => Promise<*>) => {
|
||||
return this.context.onSubmit({
|
||||
form,
|
||||
sendData: () => {
|
||||
const data = form.serialize();
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
return accounts.requestEmailChange(data).catch(handleErrors());
|
||||
case 1:
|
||||
return accounts.setNewEmail(data).catch(handleErrors('/profile/change-email'));
|
||||
case 2:
|
||||
return accounts.confirmNewEmail(data).catch(handleErrors('/profile/change-email'));
|
||||
default:
|
||||
throw new Error(`Unsupported step ${step}`);
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
step > 1 && this.context.goToProfile();
|
||||
sendData
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleErrors(repeatUrl) {
|
||||
return (resp) => {
|
||||
if (resp.errors) {
|
||||
if (resp.errors.key) {
|
||||
resp.errors.key = {
|
||||
type: resp.errors.key,
|
||||
payload: {}
|
||||
};
|
||||
|
||||
if (['error.key_not_exists', 'error.key_expire'].includes(resp.errors.key.type) && repeatUrl) {
|
||||
Object.assign(resp.errors.key.payload, {
|
||||
repeatUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
onComplete = () => {
|
||||
this.context.goToProfile();
|
||||
};
|
||||
}
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export default connect((state) => ({
|
||||
email: state.user.email,
|
||||
lang: state.user.lang
|
||||
}), {
|
||||
})(MultiFactorAuthPage);
|
||||
export default MultiFactorAuthPage;
|
||||
|
@ -5,15 +5,13 @@ import PropTypes from 'prop-types';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
|
||||
import logger from 'services/logger';
|
||||
import { browserHistory } from 'services/history';
|
||||
import { FooterMenu } from 'components/footerMenu';
|
||||
import Profile from 'components/profile/Profile';
|
||||
import ChangePasswordPage from 'pages/profile/ChangePasswordPage';
|
||||
import ChangeUsernamePage from 'pages/profile/ChangeUsernamePage';
|
||||
import ChangeEmailPage from 'pages/profile/ChangeEmailPage';
|
||||
import MultiFactorAuthPage from 'pages/profile/MultiFactorAuthPage';
|
||||
|
||||
import { FooterMenu } from 'components/footerMenu';
|
||||
|
||||
import styles from './profile.scss';
|
||||
|
||||
import type { FormModel } from 'components/ui/form';
|
||||
@ -67,7 +65,10 @@ import PasswordRequestForm from 'components/profile/passwordRequestForm/Password
|
||||
|
||||
export default connect(null, {
|
||||
fetchUserData,
|
||||
onSubmit: ({form, sendData}) => (dispatch) => {
|
||||
onSubmit: ({form, sendData}: {
|
||||
form: FormModel,
|
||||
sendData: () => Promise<*>
|
||||
}) => (dispatch) => {
|
||||
form.beginLoading();
|
||||
return sendData()
|
||||
.catch((resp) => {
|
||||
@ -76,7 +77,7 @@ export default connect(null, {
|
||||
// prevalidate user input, because requestPassword popup will block the
|
||||
// entire form from input, so it must be valid
|
||||
if (resp.errors) {
|
||||
Reflect.deleteProperty(resp.errors, 'password');
|
||||
delete resp.errors.password;
|
||||
|
||||
if (resp.errors.email && resp.data && resp.data.canRepeatIn) {
|
||||
resp.errors.email = {
|
||||
@ -92,10 +93,13 @@ export default connect(null, {
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
|
||||
return Promise.resolve({requirePassword});
|
||||
if (requirePassword) {
|
||||
return requestPassword(form);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
})
|
||||
.then((resp) => !resp.requirePassword || requestPassword(form))
|
||||
.catch((resp) => {
|
||||
if (!resp || !resp.errors) {
|
||||
logger.warn('Unexpected profile editing error', {
|
||||
|
@ -5,5 +5,21 @@ import type { Resp } from 'services/request';
|
||||
export default {
|
||||
getSecret(): Promise<Resp<{qr: string, secret: string, uri: string}>> {
|
||||
return request.get('/api/two-factor-auth');
|
||||
},
|
||||
|
||||
enable(data: {totp: string, password?: string}): Promise<Resp<*>> {
|
||||
return request.post('/api/two-factor-auth', {
|
||||
token: data.totp,
|
||||
password: data.password || ''
|
||||
}).catch((resp) => {
|
||||
if (resp.errors) {
|
||||
if (resp.errors.token) {
|
||||
resp.errors.totp = resp.errors.token.replace('token', 'totp');
|
||||
delete resp.errors.token;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -22,6 +22,9 @@
|
||||
"passwordTooShort": "Your password should be at least 8 characters length",
|
||||
"passwordsDoesNotMatch": "The passwords does not match",
|
||||
"rulesAgreementRequired": "You must accept rules in order to create an account",
|
||||
"totpRequired": "Please, enter the code",
|
||||
"totpIncorrect": "The code is incorrect",
|
||||
"mfaAlreadyEnabled": "The two factor auth is already enabled",
|
||||
"keyRequired": "Please, enter an activation key",
|
||||
"keyNotExists": "The key is incorrect or has expired.",
|
||||
"doYouWantRequestKey": "Do you want to request a new key?",
|
||||
|
@ -50,6 +50,10 @@ const errorsMap = {
|
||||
</span>
|
||||
),
|
||||
|
||||
'error.totp_required': () => <Message {...messages.totpRequired} />,
|
||||
'error.totp_incorrect': () => <Message {...messages.totpIncorrect} />,
|
||||
'error.otp_already_enabled': () => <Message {...messages.mfaAlreadyEnabled} />,
|
||||
|
||||
'error.rePassword_required': () => <Message {...messages.rePasswordRequired} />,
|
||||
'error.password_too_short': () => <Message {...messages.passwordTooShort} />,
|
||||
'error.rePassword_does_not_match': () => <Message {...messages.passwordsDoesNotMatch} />,
|
||||
|
Loading…
x
Reference in New Issue
Block a user