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