mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
#305: implement disable mfa form
This commit is contained in:
12
.eslintrc
12
.eslintrc
@@ -6,7 +6,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react"
|
"react",
|
||||||
|
"flowtype"
|
||||||
],
|
],
|
||||||
|
|
||||||
"env": {
|
"env": {
|
||||||
@@ -16,7 +17,10 @@
|
|||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"extends": "eslint:recommended",
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:flowtype/recommended"
|
||||||
|
],
|
||||||
|
|
||||||
// @see: http://eslint.org/docs/rules/
|
// @see: http://eslint.org/docs/rules/
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -200,6 +204,8 @@
|
|||||||
"react/prefer-es6-class": "warn",
|
"react/prefer-es6-class": "warn",
|
||||||
"react/prop-types": "off", // using flowtype for this task
|
"react/prop-types": "off", // using flowtype for this task
|
||||||
"react/self-closing-comp": "warn",
|
"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": "^1.2.2",
|
||||||
"intl-format-cache": "^2.0.4",
|
"intl-format-cache": "^2.0.4",
|
||||||
"intl-messageformat": "^2.1.0",
|
"intl-messageformat": "^2.1.0",
|
||||||
"promise.prototype.finally": "^3.0.0",
|
"promise.prototype.finally": "3.0.1",
|
||||||
"raven-js": "^3.8.1",
|
"raven-js": "^3.8.1",
|
||||||
"react": "^15.0.0",
|
"react": "^15.0.0",
|
||||||
"react-dom": "^15.0.0",
|
"react-dom": "^15.0.0",
|
||||||
@@ -67,11 +67,12 @@
|
|||||||
"css-loader": "^0.28.0",
|
"css-loader": "^0.28.0",
|
||||||
"enzyme": "^2.2.0",
|
"enzyme": "^2.2.0",
|
||||||
"eslint": "^4.0.0",
|
"eslint": "^4.0.0",
|
||||||
|
"eslint-plugin-flowtype": "2.35.1",
|
||||||
"eslint-plugin-react": "^7.3.0",
|
"eslint-plugin-react": "^7.3.0",
|
||||||
"exports-loader": "^0.6.3",
|
"exports-loader": "^0.6.3",
|
||||||
"extract-text-webpack-plugin": "^1.0.0",
|
"extract-text-webpack-plugin": "^1.0.0",
|
||||||
"file-loader": "^0.11.0",
|
"file-loader": "^0.11.0",
|
||||||
"flow-bin": "^0.53.1",
|
"flow-bin": "0.54.1",
|
||||||
"fontgen-loader": "^0.2.1",
|
"fontgen-loader": "^0.2.1",
|
||||||
"html-loader": "^0.4.3",
|
"html-loader": "^0.4.3",
|
||||||
"html-webpack-plugin": "^2.0.0",
|
"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();
|
loader.show();
|
||||||
|
|
||||||
return () => new Promise(() => {
|
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) => {
|
return (resp) => {
|
||||||
if (resp.errors) {
|
if (resp.errors) {
|
||||||
const firstError = Object.keys(resp.errors)[0];
|
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",
|
"codePlaceholder": "Enter the code here",
|
||||||
"enterCodeFromApp": "In order to finish two-factor auth setup, please enter the code received in mobile app:",
|
"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
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage as Message } from 'react-intl';
|
|
||||||
import Helmet from 'react-helmet';
|
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 styles from 'components/profile/profileForm.scss';
|
||||||
import Stepper from 'components/ui/stepper';
|
import { BackButton } from 'components/profile/ProfileForm';
|
||||||
import { ScrollMotion } from 'components/ui/motion';
|
|
||||||
import logger from 'services/logger';
|
|
||||||
import mfa from 'services/api/mfa';
|
|
||||||
|
|
||||||
import Instructions from './instructions';
|
import MfaEnable from './MfaEnable';
|
||||||
import KeyForm from './keyForm';
|
import MfaDisable from './MfaDisable';
|
||||||
import Confirmation from './confirmation';
|
|
||||||
import messages from './MultiFactorAuth.intl.json';
|
import messages from './MultiFactorAuth.intl.json';
|
||||||
|
|
||||||
import type { Form } from 'components/ui/form';
|
import type { MfaStep } from './MfaEnable';
|
||||||
|
|
||||||
const STEPS_TOTAL = 3;
|
class MultiFactorAuth extends Component<{
|
||||||
|
step: MfaStep,
|
||||||
export type MfaStep = 0|1|2;
|
isMfaEnabled: bool,
|
||||||
type Props = {
|
onSubmit: Function,
|
||||||
onChangeStep: Function,
|
|
||||||
lang: string,
|
|
||||||
email: string,
|
|
||||||
confirmationForm: FormModel,
|
|
||||||
onSubmit: (form: FormModel, sendData: () => Promise<*>) => Promise<*>,
|
|
||||||
onComplete: Function,
|
onComplete: Function,
|
||||||
step: MfaStep
|
onChangeStep: Function
|
||||||
};
|
|
||||||
|
|
||||||
export default class MultiFactorAuth extends Component<Props, {
|
|
||||||
isLoading: bool,
|
|
||||||
activeStep: MfaStep,
|
|
||||||
secret: string,
|
|
||||||
qrCodeSrc: string
|
|
||||||
}> {
|
}> {
|
||||||
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() {
|
render() {
|
||||||
const {activeStep, isLoading} = this.state;
|
const {
|
||||||
|
step,
|
||||||
const stepsData = [
|
onSubmit,
|
||||||
{
|
onComplete,
|
||||||
buttonLabel: messages.theAppIsInstalled,
|
onChangeStep,
|
||||||
buttonAction: () => this.nextStep()
|
isMfaEnabled
|
||||||
},
|
} = this.props;
|
||||||
{
|
|
||||||
buttonLabel: messages.ready,
|
|
||||||
buttonAction: () => this.nextStep()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
buttonLabel: messages.enableTwoFactorAuth,
|
|
||||||
buttonAction: () => this.confirmationFormEl && this.confirmationFormEl.submit()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const {buttonLabel, buttonAction} = stepsData[activeStep];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.contentWithBackButton}>
|
<div className={styles.contentWithBackButton}>
|
||||||
@@ -106,99 +49,26 @@ export default class MultiFactorAuth extends Component<Props, {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isMfaEnabled && (
|
||||||
|
<MfaDisable
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onComplete={onComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.stepper}>
|
{isMfaEnabled || (
|
||||||
<Stepper totalSteps={STEPS_TOTAL} activeStep={activeStep} />
|
<MfaEnable
|
||||||
</div>
|
step={step}
|
||||||
|
onSubmit={onSubmit}
|
||||||
<div className={styles.form}>
|
onChangeStep={onChangeStep}
|
||||||
{this.renderStepForms()}
|
onComplete={onComplete}
|
||||||
|
|
||||||
<Button
|
|
||||||
color="green"
|
|
||||||
onClick={buttonAction}
|
|
||||||
loading={isLoading}
|
|
||||||
block
|
|
||||||
label={buttonLabel}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</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()) {
|
if (form.checkValidity()) {
|
||||||
|
this.setState({isLoading: true});
|
||||||
|
|
||||||
Promise.resolve(this.props.onSubmit(
|
Promise.resolve(this.props.onSubmit(
|
||||||
this.props.form ? this.props.form : new FormData(form)
|
this.props.form ? this.props.form : new FormData(form)
|
||||||
))
|
))
|
||||||
.catch((errors: {[key: string]: string}) => {
|
.catch((errors: {[key: string]: string}) => {
|
||||||
this.setErrors(errors);
|
this.setErrors(errors);
|
||||||
});
|
})
|
||||||
|
.finally(() => this.setState({isLoading: false}));
|
||||||
} else {
|
} else {
|
||||||
const invalidEls = form.querySelectorAll(':invalid');
|
const invalidEls = form.querySelectorAll(':invalid');
|
||||||
const errors = {};
|
const errors = {};
|
||||||
@@ -129,6 +132,7 @@ export default class Form extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let errorMessage = el.validationMessage;
|
let errorMessage = el.validationMessage;
|
||||||
|
|
||||||
if (el.validity.valueMissing) {
|
if (el.validity.valueMissing) {
|
||||||
errorMessage = `error.${el.name}_required`;
|
errorMessage = `error.${el.name}_required`;
|
||||||
} else if (el.validity.typeMismatch) {
|
} else if (el.validity.typeMismatch) {
|
||||||
|
@@ -101,7 +101,7 @@ function createOnOutsideComponentClickHandler(
|
|||||||
// TODO: we have the same logic in LangMenu
|
// TODO: we have the same logic in LangMenu
|
||||||
// Probably we should decouple this into some helper function
|
// Probably we should decouple this into some helper function
|
||||||
// TODO: the name of function may be better...
|
// TODO: the name of function may be better...
|
||||||
return (event: MouseEvent & {target: HTMLElement}) => {
|
return (event: {target: HTMLElement} & MouseEvent) => {
|
||||||
const el = getEl();
|
const el = getEl();
|
||||||
|
|
||||||
if (isActive() && el) {
|
if (isActive() && el) {
|
||||||
|
@@ -1,18 +1,21 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 { FormModel } from 'components/ui/form';
|
||||||
|
import type { User } from 'components/user';
|
||||||
|
|
||||||
class MultiFactorAuthPage extends Component<{
|
class MultiFactorAuthPage extends Component<{
|
||||||
|
user: User,
|
||||||
history: {
|
history: {
|
||||||
push: (string) => void
|
push: (string) => void
|
||||||
},
|
},
|
||||||
match: {
|
match: {
|
||||||
params: {
|
params: {
|
||||||
step?: '1'|'2'|'3'
|
step?: '1' | '2' | '3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}> {
|
}> {
|
||||||
@@ -23,18 +26,28 @@ class MultiFactorAuthPage extends Component<{
|
|||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const step = this.props.match.params.step;
|
const step = this.props.match.params.step;
|
||||||
|
const {user} = this.props;
|
||||||
|
|
||||||
if (step && !/^[1-3]$/.test(step)) {
|
if (step) {
|
||||||
// wrong param value
|
if (!/^[1-3]$/.test(step)) {
|
||||||
this.props.history.push('/404');
|
// wrong param value
|
||||||
|
this.props.history.push('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isOtpEnabled) {
|
||||||
|
this.props.history.push('/mfa');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const step = (parseInt(this.props.match.params.step, 10) || 1) - 1;
|
const step = (parseInt(this.props.match.params.step, 10) || 1) - 1;
|
||||||
|
const {user} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiFactorAuth
|
<MultiFactorAuth
|
||||||
|
isMfaEnabled={user.isOtpEnabled}
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
step={step}
|
step={step}
|
||||||
onChangeStep={this.onChangeStep}
|
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
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
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 logger from 'services/logger';
|
||||||
import { browserHistory } from 'services/history';
|
import { browserHistory } from 'services/history';
|
||||||
import { FooterMenu } from 'components/footerMenu';
|
import { FooterMenu } from 'components/footerMenu';
|
||||||
@@ -57,11 +60,6 @@ class ProfilePage extends Component<{
|
|||||||
goToProfile = () => browserHistory.push('/');
|
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, {
|
export default connect(null, {
|
||||||
fetchUserData,
|
fetchUserData,
|
||||||
onSubmit: ({form, sendData}: {
|
onSubmit: ({form, sendData}: {
|
||||||
@@ -69,6 +67,7 @@ export default connect(null, {
|
|||||||
sendData: () => Promise<*>
|
sendData: () => Promise<*>
|
||||||
}) => (dispatch) => {
|
}) => (dispatch) => {
|
||||||
form.beginLoading();
|
form.beginLoading();
|
||||||
|
|
||||||
return sendData()
|
return sendData()
|
||||||
.catch((resp) => {
|
.catch((resp) => {
|
||||||
const requirePassword = resp.errors && !!resp.errors.password;
|
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
|
if (event.defaultPrevented
|
||||||
|| !event.currentTarget.id
|
|| !event.currentTarget.id
|
||||||
|| event.target.tagName.toLowerCase() === 'a'
|
|| event.target instanceof HTMLAnchorElement
|
||||||
) {
|
) {
|
||||||
// some-one have already processed this event or it is a link
|
// some-one have already processed this event or it is a link
|
||||||
return;
|
return;
|
||||||
|
@@ -27,9 +27,7 @@ describe('RulesPage', () => {
|
|||||||
const expectedUrl = `/foo?bar#${id}`;
|
const expectedUrl = `/foo?bar#${id}`;
|
||||||
|
|
||||||
page.find(`#${id}`).simulate('click', {
|
page.find(`#${id}`).simulate('click', {
|
||||||
target: {
|
target: document.createElement('li'),
|
||||||
tagName: 'li'
|
|
||||||
},
|
|
||||||
|
|
||||||
currentTarget: {
|
currentTarget: {
|
||||||
id
|
id
|
||||||
@@ -41,9 +39,7 @@ describe('RulesPage', () => {
|
|||||||
|
|
||||||
it('should not update location if link was clicked', () => {
|
it('should not update location if link was clicked', () => {
|
||||||
page.find(`#${id}`).simulate('click', {
|
page.find(`#${id}`).simulate('click', {
|
||||||
target: {
|
target: document.createElement('a'),
|
||||||
tagName: 'a'
|
|
||||||
},
|
|
||||||
|
|
||||||
currentTarget: {
|
currentTarget: {
|
||||||
id
|
id
|
||||||
|
@@ -16,16 +16,9 @@ const authentication = {
|
|||||||
}) {
|
}) {
|
||||||
return request.post(
|
return request.post(
|
||||||
'/api/authentication/login',
|
'/api/authentication/login',
|
||||||
{login, password, token: totp, rememberMe},
|
{login, password, totp, rememberMe},
|
||||||
{token: null}
|
{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<*>> {
|
enable(data: {totp: string, password?: string}): Promise<Resp<*>> {
|
||||||
return request.post('/api/two-factor-auth', {
|
return request.post('/api/two-factor-auth', {
|
||||||
token: data.totp,
|
totp: data.totp,
|
||||||
password: data.password || ''
|
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}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
|
get<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
|
||||||
if (typeof data === 'object' && Object.keys(data).length) {
|
url = buildUrl(url, data);
|
||||||
const separator = url.indexOf('?') === -1 ? '?' : '&';
|
|
||||||
url += separator + buildQuery(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return doFetch(url, options);
|
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
|
* Serializes object into encoded key=value presentation
|
||||||
*
|
*
|
||||||
@@ -75,7 +90,6 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const checkStatus = (resp) => resp.status >= 200 && resp.status < 300
|
const checkStatus = (resp) => resp.status >= 200 && resp.status < 300
|
||||||
? Promise.resolve(resp)
|
? Promise.resolve(resp)
|
||||||
: Promise.reject(resp);
|
: Promise.reject(resp);
|
||||||
@@ -160,3 +174,12 @@ function buildQuery(data: Object = {}): string {
|
|||||||
)
|
)
|
||||||
.join('&');
|
.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;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user