#305: fully connect mfa ui with backend

This commit is contained in:
SleepWalker 2017-08-20 18:45:21 +03:00
parent b98be6b737
commit 42399ef9bf
15 changed files with 275 additions and 274 deletions

View File

@ -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"

View File

@ -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

View File

@ -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);
// }
// });
}; };
} }

View File

@ -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>
); );
} }

View File

@ -1 +1 @@
export { default } from './MultiFactorAuth'; export { default, MfaStep } from './MultiFactorAuth';

View File

@ -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;
}

View File

@ -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>
); );
} }

View File

@ -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});
} }

View File

@ -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;

View File

@ -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,

View File

@ -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);

View File

@ -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', {

View File

@ -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);
});
} }
}; };

View File

@ -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?",

View File

@ -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} />,