mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-23 00:22:57 +05:30
#305: implement disable mfa form
This commit is contained in:
parent
02ce9bb3b5
commit
d1b19a2285
12
.eslintrc
12
.eslintrc
@ -6,7 +6,8 @@
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
"react"
|
||||
"react",
|
||||
"flowtype"
|
||||
],
|
||||
|
||||
"env": {
|
||||
@ -16,7 +17,10 @@
|
||||
"es6": true
|
||||
},
|
||||
|
||||
"extends": "eslint:recommended",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
|
||||
// @see: http://eslint.org/docs/rules/
|
||||
"rules": {
|
||||
@ -200,6 +204,8 @@
|
||||
"react/prefer-es6-class": "warn",
|
||||
"react/prop-types": "off", // using flowtype for this task
|
||||
"react/self-closing-comp": "warn",
|
||||
"react/sort-comp": ["off", {"order": ["lifecycle", "render", "everything-else"]}]
|
||||
"react/sort-comp": ["off", {"order": ["lifecycle", "render", "everything-else"]}],
|
||||
|
||||
"flowtype/boolean-style": ["error", "bool"],
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
"intl": "^1.2.2",
|
||||
"intl-format-cache": "^2.0.4",
|
||||
"intl-messageformat": "^2.1.0",
|
||||
"promise.prototype.finally": "^3.0.0",
|
||||
"promise.prototype.finally": "3.0.1",
|
||||
"raven-js": "^3.8.1",
|
||||
"react": "^15.0.0",
|
||||
"react-dom": "^15.0.0",
|
||||
@ -67,11 +67,12 @@
|
||||
"css-loader": "^0.28.0",
|
||||
"enzyme": "^2.2.0",
|
||||
"eslint": "^4.0.0",
|
||||
"eslint-plugin-flowtype": "2.35.1",
|
||||
"eslint-plugin-react": "^7.3.0",
|
||||
"exports-loader": "^0.6.3",
|
||||
"extract-text-webpack-plugin": "^1.0.0",
|
||||
"file-loader": "^0.11.0",
|
||||
"flow-bin": "^0.53.1",
|
||||
"flow-bin": "0.54.1",
|
||||
"fontgen-loader": "^0.2.1",
|
||||
"html-loader": "^0.4.3",
|
||||
"html-webpack-plugin": "^2.0.0",
|
||||
|
@ -36,7 +36,7 @@ export function goBack(fallbackUrl?: ?string = null) {
|
||||
};
|
||||
}
|
||||
|
||||
export function redirect(url: string) {
|
||||
export function redirect(url: string): () => Promise<*> {
|
||||
loader.show();
|
||||
|
||||
return () => new Promise(() => {
|
||||
@ -508,7 +508,7 @@ function authHandler(dispatch) {
|
||||
});
|
||||
}
|
||||
|
||||
function validationErrorsHandler(dispatch: (Function|Object) => void, repeatUrl?: string) {
|
||||
function validationErrorsHandler(dispatch: (Function | Object) => void, repeatUrl?: string) {
|
||||
return (resp) => {
|
||||
if (resp.errors) {
|
||||
const firstError = Object.keys(resp.errors)[0];
|
||||
|
54
src/components/profile/multiFactorAuth/MfaDisable.js
Normal file
54
src/components/profile/multiFactorAuth/MfaDisable.js
Normal file
@ -0,0 +1,54 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import logger from 'services/logger';
|
||||
import mfa from 'services/api/mfa';
|
||||
|
||||
import MfaDisableForm from './disableForm/MfaDisableForm';
|
||||
import MfaStatus from './status/MfaStatus';
|
||||
|
||||
import type { FormModel } from 'components/ui/form';
|
||||
|
||||
export default class MfaDisable extends Component<{
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
||||
onComplete: Function
|
||||
}, {
|
||||
showForm?: bool
|
||||
}> {
|
||||
state = {};
|
||||
|
||||
render() {
|
||||
const { showForm } = this.state;
|
||||
|
||||
return showForm ? (
|
||||
<MfaDisableForm onSubmit={this.onSubmit}/>
|
||||
) : (
|
||||
<MfaStatus onProceed={this.onProceed} />
|
||||
);
|
||||
}
|
||||
|
||||
onProceed = () => this.setState({showForm: true});
|
||||
|
||||
onSubmit = (form: FormModel) => {
|
||||
return this.props.onSubmit(
|
||||
form,
|
||||
() => {
|
||||
const data = form.serialize();
|
||||
|
||||
return mfa.disable(data);
|
||||
}
|
||||
)
|
||||
.then(() => this.props.onComplete())
|
||||
.catch((resp) => {
|
||||
const {errors} = resp || {};
|
||||
|
||||
if (errors) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
|
||||
logger.error('MFA: Unexpected disable form result', {
|
||||
resp
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
171
src/components/profile/multiFactorAuth/MfaEnable.js
Normal file
171
src/components/profile/multiFactorAuth/MfaEnable.js
Normal file
@ -0,0 +1,171 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Button, FormModel } from 'components/ui/form';
|
||||
import styles from 'components/profile/profileForm.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';
|
||||
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,
|
||||
confirmationForm: FormModel,
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
||||
onComplete: Function,
|
||||
step: MfaStep
|
||||
};
|
||||
|
||||
export default class MfaEnable extends Component<Props, {
|
||||
isLoading: bool,
|
||||
activeStep: MfaStep,
|
||||
secret: string,
|
||||
qrCodeSrc: string
|
||||
}> {
|
||||
static defaultProps = {
|
||||
confirmationForm: new FormModel(),
|
||||
step: 0
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: false,
|
||||
activeStep: this.props.step,
|
||||
qrCodeSrc: '',
|
||||
secret: ''
|
||||
};
|
||||
|
||||
confirmationFormEl: ?Form;
|
||||
|
||||
componentWillMount() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.syncState(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {activeStep, isLoading} = this.state;
|
||||
|
||||
const stepsData = [
|
||||
{
|
||||
buttonLabel: messages.theAppIsInstalled,
|
||||
buttonAction: () => this.nextStep()
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.ready,
|
||||
buttonAction: () => this.nextStep()
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.enableTwoFactorAuth,
|
||||
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}>
|
||||
{this.renderStepForms()}
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={buttonAction}
|
||||
loading={isLoading}
|
||||
block
|
||||
label={buttonLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStepForms() {
|
||||
const {activeStep, secret, qrCodeSrc} = this.state;
|
||||
|
||||
return (
|
||||
<ScrollMotion activeStep={activeStep}>
|
||||
{[
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
syncState(props: Props) {
|
||||
if (props.step === 1) {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
mfa.getSecret().then((resp) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
secret: resp.secret,
|
||||
qrCodeSrc: resp.qr
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
activeStep: typeof props.step === 'number' ? props.step : this.state.activeStep
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
const nextStep = this.state.activeStep + 1;
|
||||
|
||||
if (nextStep < STEPS_TOTAL) {
|
||||
this.props.onChangeStep(nextStep);
|
||||
}
|
||||
}
|
||||
|
||||
onTotpSubmit = (form: FormModel): Promise<*> => {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
return this.props.onSubmit(
|
||||
form,
|
||||
() => {
|
||||
const data = form.serialize();
|
||||
|
||||
return mfa.enable(data);
|
||||
}
|
||||
)
|
||||
.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}));
|
||||
};
|
||||
}
|
@ -15,5 +15,11 @@
|
||||
|
||||
"codePlaceholder": "Enter the code here",
|
||||
"enterCodeFromApp": "In order to finish two-factor auth setup, please enter the code received in mobile app:",
|
||||
"enableTwoFactorAuth": "Enable two-factor auth"
|
||||
"enableTwoFactorAuth": "Enable two-factor auth",
|
||||
|
||||
"disable": "Disable",
|
||||
"mfaEnabledForYourAcc": "The two-factor authentication is enabled for yout account",
|
||||
"mfaLoginFlowDesc": "In order to log in next time, you'll need to enter additional code. Please note, that Minecraft authorization won't work with two-factor auth enabled.",
|
||||
"disableMfa": "Disable two-factor authentication",
|
||||
"disableMfaInstruction": "In order to disable two-factor authentication, you need to provide a code from your mobile app and finally confirm your action with your current account password."
|
||||
}
|
||||
|
@ -1,89 +1,32 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
import Helmet from 'react-helmet';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { Button, FormModel } from 'components/ui/form';
|
||||
import { BackButton } from 'components/profile/ProfileForm';
|
||||
import styles from 'components/profile/profileForm.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 { BackButton } from 'components/profile/ProfileForm';
|
||||
|
||||
import Instructions from './instructions';
|
||||
import KeyForm from './keyForm';
|
||||
import Confirmation from './confirmation';
|
||||
import MfaEnable from './MfaEnable';
|
||||
import MfaDisable from './MfaDisable';
|
||||
import messages from './MultiFactorAuth.intl.json';
|
||||
|
||||
import type { Form } from 'components/ui/form';
|
||||
import type { MfaStep } from './MfaEnable';
|
||||
|
||||
const STEPS_TOTAL = 3;
|
||||
|
||||
export type MfaStep = 0|1|2;
|
||||
type Props = {
|
||||
onChangeStep: Function,
|
||||
lang: string,
|
||||
email: string,
|
||||
confirmationForm: FormModel,
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
||||
class MultiFactorAuth extends Component<{
|
||||
step: MfaStep,
|
||||
isMfaEnabled: bool,
|
||||
onSubmit: Function,
|
||||
onComplete: Function,
|
||||
step: MfaStep
|
||||
};
|
||||
|
||||
export default class MultiFactorAuth extends Component<Props, {
|
||||
isLoading: bool,
|
||||
activeStep: MfaStep,
|
||||
secret: string,
|
||||
qrCodeSrc: string
|
||||
onChangeStep: Function
|
||||
}> {
|
||||
static defaultProps = {
|
||||
confirmationForm: new FormModel(),
|
||||
step: 0
|
||||
};
|
||||
|
||||
state: {
|
||||
isLoading: bool,
|
||||
activeStep: MfaStep,
|
||||
secret: string,
|
||||
qrCodeSrc: string
|
||||
} = {
|
||||
isLoading: false,
|
||||
activeStep: this.props.step,
|
||||
qrCodeSrc: '',
|
||||
secret: ''
|
||||
};
|
||||
|
||||
confirmationFormEl: ?Form;
|
||||
|
||||
componentWillMount() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.syncState(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {activeStep, isLoading} = this.state;
|
||||
|
||||
const stepsData = [
|
||||
{
|
||||
buttonLabel: messages.theAppIsInstalled,
|
||||
buttonAction: () => this.nextStep()
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.ready,
|
||||
buttonAction: () => this.nextStep()
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.enableTwoFactorAuth,
|
||||
buttonAction: () => this.confirmationFormEl && this.confirmationFormEl.submit()
|
||||
}
|
||||
];
|
||||
|
||||
const {buttonLabel, buttonAction} = stepsData[activeStep];
|
||||
const {
|
||||
step,
|
||||
onSubmit,
|
||||
onComplete,
|
||||
onChangeStep,
|
||||
isMfaEnabled
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.contentWithBackButton}>
|
||||
@ -106,99 +49,26 @@ export default class MultiFactorAuth extends Component<Props, {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMfaEnabled && (
|
||||
<MfaDisable
|
||||
onSubmit={onSubmit}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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}
|
||||
{isMfaEnabled || (
|
||||
<MfaEnable
|
||||
step={step}
|
||||
onSubmit={onSubmit}
|
||||
onChangeStep={onChangeStep}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStepForms() {
|
||||
const {activeStep, secret, qrCodeSrc} = this.state;
|
||||
|
||||
return (
|
||||
<ScrollMotion activeStep={activeStep}>
|
||||
{[
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
syncState(props: Props) {
|
||||
if (props.step === 1) {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
mfa.getSecret().then((resp) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
secret: resp.secret,
|
||||
qrCodeSrc: resp.qr
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
activeStep: typeof props.step === 'number' ? props.step : this.state.activeStep
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
const nextStep = this.state.activeStep + 1;
|
||||
|
||||
if (nextStep < STEPS_TOTAL) {
|
||||
this.props.onChangeStep(nextStep);
|
||||
} else {
|
||||
this.props.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
onTotpSubmit = (form: FormModel): Promise<*> => {
|
||||
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);
|
||||
}
|
||||
|
||||
logger.error('MFA: Unexpected form submit result', {
|
||||
resp
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({isLoading: false}));
|
||||
};
|
||||
}
|
||||
|
||||
export default MultiFactorAuth;
|
||||
|
@ -0,0 +1,57 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { Button, Input, Form, FormModel } from 'components/ui/form';
|
||||
import styles from 'components/profile/profileForm.scss';
|
||||
|
||||
import messages from '../MultiFactorAuth.intl.json';
|
||||
import mfaStyles from '../mfa.scss';
|
||||
|
||||
export default class MfaDisableForm extends Component<{
|
||||
onSubmit: (form: FormModel) => Promise<*>
|
||||
}> {
|
||||
form: FormModel = new FormModel();
|
||||
|
||||
render() {
|
||||
const { form } = this;
|
||||
const {onSubmit} = this.props;
|
||||
|
||||
return (
|
||||
<Form form={form}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className={styles.formBody}>
|
||||
<div className={styles.formRow}>
|
||||
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
|
||||
<Message {...messages.disableMfa} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.disableMfaInstruction} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<Input {...form.bindField('totp')}
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
color="green"
|
||||
block
|
||||
label={messages.disable}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export { default, MfaStep } from './MultiFactorAuth';
|
||||
export { default } from './MultiFactorAuth';
|
||||
export type { MfaStep } from './MfaEnable';
|
||||
|
21
src/components/profile/multiFactorAuth/mfa.scss
Normal file
21
src/components/profile/multiFactorAuth/mfa.scss
Normal file
@ -0,0 +1,21 @@
|
||||
@import '~components/ui/colors.scss';
|
||||
@import '~components/ui/fonts.scss';
|
||||
|
||||
.mfaTitle {
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
text-align: center;
|
||||
margin-left: 60px;
|
||||
margin-right: 60px;
|
||||
}
|
||||
|
||||
.bigIcon {
|
||||
color: $blue;
|
||||
font-size: 100px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.disableMfa {
|
||||
text-align: center;
|
||||
}
|
43
src/components/profile/multiFactorAuth/status/MfaStatus.js
Normal file
43
src/components/profile/multiFactorAuth/status/MfaStatus.js
Normal file
@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import styles from 'components/profile/profileForm.scss';
|
||||
import icons from 'components/ui/icons.scss';
|
||||
|
||||
import messages from '../MultiFactorAuth.intl.json';
|
||||
import mfaStyles from '../mfa.scss';
|
||||
|
||||
export default function MfaStatus({onProceed} : {
|
||||
onProceed: Function
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.formBody}>
|
||||
<div className={styles.formRow}>
|
||||
<div className={mfaStyles.bigIcon}>
|
||||
<span className={icons.lock} />
|
||||
</div>
|
||||
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
|
||||
<Message {...messages.mfaEnabledForYourAcc} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaLoginFlowDesc} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.formRow} ${mfaStyles.disableMfa}`}>
|
||||
<p className={styles.description}>
|
||||
<a href="#" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onProceed();
|
||||
}}>
|
||||
<Message {...messages.disable} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -110,12 +110,15 @@ export default class Form extends Component<Props, State> {
|
||||
}
|
||||
|
||||
if (form.checkValidity()) {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
Promise.resolve(this.props.onSubmit(
|
||||
this.props.form ? this.props.form : new FormData(form)
|
||||
))
|
||||
.catch((errors: {[key: string]: string}) => {
|
||||
this.setErrors(errors);
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({isLoading: false}));
|
||||
} else {
|
||||
const invalidEls = form.querySelectorAll(':invalid');
|
||||
const errors = {};
|
||||
@ -129,6 +132,7 @@ export default class Form extends Component<Props, State> {
|
||||
}
|
||||
|
||||
let errorMessage = el.validationMessage;
|
||||
|
||||
if (el.validity.valueMissing) {
|
||||
errorMessage = `error.${el.name}_required`;
|
||||
} else if (el.validity.typeMismatch) {
|
||||
|
@ -101,7 +101,7 @@ function createOnOutsideComponentClickHandler(
|
||||
// TODO: we have the same logic in LangMenu
|
||||
// Probably we should decouple this into some helper function
|
||||
// TODO: the name of function may be better...
|
||||
return (event: MouseEvent & {target: HTMLElement}) => {
|
||||
return (event: {target: HTMLElement} & MouseEvent) => {
|
||||
const el = getEl();
|
||||
|
||||
if (isActive() && el) {
|
||||
|
@ -1,18 +1,21 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import MultiFactorAuth, { MfaStep } from 'components/profile/multiFactorAuth';
|
||||
|
||||
import MultiFactorAuth from 'components/profile/multiFactorAuth';
|
||||
import type { MfaStep } from 'components/profile/multiFactorAuth';
|
||||
import type { FormModel } from 'components/ui/form';
|
||||
import type { User } from 'components/user';
|
||||
|
||||
class MultiFactorAuthPage extends Component<{
|
||||
user: User,
|
||||
history: {
|
||||
push: (string) => void
|
||||
},
|
||||
match: {
|
||||
params: {
|
||||
step?: '1'|'2'|'3'
|
||||
step?: '1' | '2' | '3'
|
||||
}
|
||||
}
|
||||
}> {
|
||||
@ -23,18 +26,28 @@ class MultiFactorAuthPage extends Component<{
|
||||
|
||||
componentWillMount() {
|
||||
const step = this.props.match.params.step;
|
||||
const {user} = this.props;
|
||||
|
||||
if (step && !/^[1-3]$/.test(step)) {
|
||||
// wrong param value
|
||||
this.props.history.push('/404');
|
||||
if (step) {
|
||||
if (!/^[1-3]$/.test(step)) {
|
||||
// wrong param value
|
||||
this.props.history.push('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.isOtpEnabled) {
|
||||
this.props.history.push('/mfa');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const step = (parseInt(this.props.match.params.step, 10) || 1) - 1;
|
||||
const {user} = this.props;
|
||||
|
||||
return (
|
||||
<MultiFactorAuth
|
||||
isMfaEnabled={user.isOtpEnabled}
|
||||
onSubmit={this.onSubmit}
|
||||
step={step}
|
||||
onChangeStep={this.onChangeStep}
|
||||
@ -59,4 +72,4 @@ class MultiFactorAuthPage extends Component<{
|
||||
};
|
||||
}
|
||||
|
||||
export default MultiFactorAuthPage;
|
||||
export default connect(({user}) => ({user}))(MultiFactorAuthPage);
|
||||
|
@ -1,9 +1,12 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchUserData } from 'components/user/actions';
|
||||
import { create as createPopup } from 'components/ui/popup/actions';
|
||||
import PasswordRequestForm from 'components/profile/passwordRequestForm/PasswordRequestForm';
|
||||
import logger from 'services/logger';
|
||||
import { browserHistory } from 'services/history';
|
||||
import { FooterMenu } from 'components/footerMenu';
|
||||
@ -57,11 +60,6 @@ class ProfilePage extends Component<{
|
||||
goToProfile = () => browserHistory.push('/');
|
||||
}
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchUserData } from 'components/user/actions';
|
||||
import { create as createPopup } from 'components/ui/popup/actions';
|
||||
import PasswordRequestForm from 'components/profile/passwordRequestForm/PasswordRequestForm';
|
||||
|
||||
export default connect(null, {
|
||||
fetchUserData,
|
||||
onSubmit: ({form, sendData}: {
|
||||
@ -69,6 +67,7 @@ export default connect(null, {
|
||||
sendData: () => Promise<*>
|
||||
}) => (dispatch) => {
|
||||
form.beginLoading();
|
||||
|
||||
return sendData()
|
||||
.catch((resp) => {
|
||||
const requirePassword = resp.errors && !!resp.errors.password;
|
||||
|
@ -128,10 +128,10 @@ export default class RulesPage extends Component<{
|
||||
);
|
||||
}
|
||||
|
||||
onRuleClick(event: MouseEvent & {target: HTMLElement, currentTarget: HTMLElement}) {
|
||||
onRuleClick(event: SyntheticMouseEvent<HTMLElement>) {
|
||||
if (event.defaultPrevented
|
||||
|| !event.currentTarget.id
|
||||
|| event.target.tagName.toLowerCase() === 'a'
|
||||
|| event.target instanceof HTMLAnchorElement
|
||||
) {
|
||||
// some-one have already processed this event or it is a link
|
||||
return;
|
||||
|
@ -27,9 +27,7 @@ describe('RulesPage', () => {
|
||||
const expectedUrl = `/foo?bar#${id}`;
|
||||
|
||||
page.find(`#${id}`).simulate('click', {
|
||||
target: {
|
||||
tagName: 'li'
|
||||
},
|
||||
target: document.createElement('li'),
|
||||
|
||||
currentTarget: {
|
||||
id
|
||||
@ -41,9 +39,7 @@ describe('RulesPage', () => {
|
||||
|
||||
it('should not update location if link was clicked', () => {
|
||||
page.find(`#${id}`).simulate('click', {
|
||||
target: {
|
||||
tagName: 'a'
|
||||
},
|
||||
target: document.createElement('a'),
|
||||
|
||||
currentTarget: {
|
||||
id
|
||||
|
@ -16,16 +16,9 @@ const authentication = {
|
||||
}) {
|
||||
return request.post(
|
||||
'/api/authentication/login',
|
||||
{login, password, token: totp, rememberMe},
|
||||
{login, password, totp, rememberMe},
|
||||
{token: null}
|
||||
).catch((resp) => {
|
||||
if (resp && resp.errors && resp.errors.token) {
|
||||
resp.errors.totp = resp.errors.token.replace('token', 'totp');
|
||||
delete resp.errors.token;
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -9,17 +9,15 @@ export default {
|
||||
|
||||
enable(data: {totp: string, password?: string}): Promise<Resp<*>> {
|
||||
return request.post('/api/two-factor-auth', {
|
||||
token: data.totp,
|
||||
totp: 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);
|
||||
disable(data: {totp: string, password?: string}): Promise<Resp<*>> {
|
||||
return request.delete('/api/two-factor-auth', {
|
||||
totp: data.totp,
|
||||
password: data.password || ''
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -41,14 +41,29 @@ export default {
|
||||
* @return {Promise}
|
||||
*/
|
||||
get<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
|
||||
if (typeof data === 'object' && Object.keys(data).length) {
|
||||
const separator = url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += separator + buildQuery(data);
|
||||
}
|
||||
url = buildUrl(url, data);
|
||||
|
||||
return doFetch(url, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {object} [data] - request data
|
||||
* @param {object} [options] - additional options for fetch or middlewares
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
delete<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
|
||||
return doFetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: buildQuery(data),
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes object into encoded key=value presentation
|
||||
*
|
||||
@ -75,7 +90,6 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const checkStatus = (resp) => resp.status >= 200 && resp.status < 300
|
||||
? Promise.resolve(resp)
|
||||
: Promise.reject(resp);
|
||||
@ -160,3 +174,12 @@ function buildQuery(data: Object = {}): string {
|
||||
)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
function buildUrl(url: string, data?: Object): string {
|
||||
if (typeof data === 'object' && Object.keys(data).length) {
|
||||
const separator = url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += separator + buildQuery(data);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user