#305: draft otp secret request integration

This commit is contained in:
SleepWalker 2017-08-01 23:00:02 +03:00
parent a8ae0e0c05
commit ba8b725f9f
8 changed files with 83 additions and 33 deletions

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "ely-by-account", "name": "ely-by-account",
"version": "1.1.19-dev", "version": "1.1.21-dev",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -3714,9 +3714,9 @@
"dev": true "dev": true
}, },
"flow-bin": { "flow-bin": {
"version": "0.47.0", "version": "0.51.1",
"resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.47.0.tgz", "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.51.1.tgz",
"integrity": "sha1-oqCKs+DR8ctX0X4nswsRi2L9o2c=", "integrity": "sha1-eSnG8KlOdlQp/LLubkaCePqpxzI=",
"dev": true "dev": true
}, },
"fontgen-loader": { "fontgen-loader": {

View File

@ -72,7 +72,7 @@
"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.47.0", "flow-bin": "^0.51.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",

View File

@ -7,7 +7,7 @@
"getAlternativeApps": "Get alternative apps", "getAlternativeApps": "Get alternative apps",
"theAppIsInstalled": "The app is installed", "theAppIsInstalled": "The app is installed",
"scanQrCode": "Open your favorit QR scanner app and scan the following QR code:", "scanQrCode": "Open your favorite QR scanner app and scan the following QR code:",
"or": "OR", "or": "OR",
"enterKeyManually": "If you can't scan QR code, then enter the secret key manually:", "enterKeyManually": "If you can't scan QR code, then enter the secret key manually:",
"whenKeyEntered": "Go to the next step, after you will see temporary code in your two-factor auth app.", "whenKeyEntered": "Go to the next step, after you will see temporary code in your two-factor auth app.",

View File

@ -10,6 +10,7 @@ import styles from 'components/profile/profileForm.scss';
import helpLinks from 'components/auth/helpLinks.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 mfa from 'services/api/mfa';
import Instructions from './instructions'; import Instructions from './instructions';
import KeyForm from './keyForm'; import KeyForm from './keyForm';
@ -39,11 +40,17 @@ export default class MultiFactorAuth extends Component {
}; };
state: { state: {
isLoading: bool,
activeStep: number, activeStep: number,
secret: string,
qrCodeSrc: string,
code: string, code: string,
newEmail: ?string newEmail: ?string
} = { } = {
isLoading: false,
activeStep: this.props.step, activeStep: this.props.step,
qrCodeSrc: '',
secret: '',
code: this.props.code || '', code: this.props.code || '',
newEmail: null newEmail: null
}; };
@ -56,7 +63,7 @@ export default class MultiFactorAuth extends Component {
} }
render() { render() {
const {activeStep} = this.state; const {activeStep, isLoading} = this.state;
const form = this.props.stepForm; const form = this.props.stepForm;
const stepsData = [ const stepsData = [
@ -76,6 +83,7 @@ export default class MultiFactorAuth extends Component {
return ( return (
<Form form={form} <Form form={form}
onSubmit={this.onFormSubmit} onSubmit={this.onFormSubmit}
isLoading={isLoading}
onInvalid={() => this.forceUpdate()} onInvalid={() => this.forceUpdate()}
> >
<div className={styles.contentWithBackButton}> <div className={styles.contentWithBackButton}>
@ -128,11 +136,16 @@ export default class MultiFactorAuth extends Component {
} }
renderStepForms() { renderStepForms() {
const {activeStep} = this.state; const {activeStep, secret, qrCodeSrc} = this.state;
const steps = [ const steps = [
() => <Instructions key="step1" />, () => <Instructions key="step1" />,
() => <KeyForm key="step2" />, () => (
<KeyForm key="step2"
secret={secret}
qrCodeSrc={qrCodeSrc}
/>
),
() => ( () => (
<Confirmation key="step3" <Confirmation key="step3"
form={this.props.stepForm} form={this.props.stepForm}
@ -183,7 +196,15 @@ export default class MultiFactorAuth extends Component {
}; };
onFormSubmit = () => { onFormSubmit = () => {
this.setState({isLoading: true});
mfa.getSecret().then((resp) => {
this.setState({
isLoading: false,
secret: resp.secret,
qrCodeSrc: `data:image/svg+xml;base64,${resp.qr}`
});
this.nextStep(); this.nextStep();
});
// const {activeStep} = this.state; // const {activeStep} = this.state;
// const form = this.props.stepForms[activeStep]; // const form = this.props.stepForms[activeStep];
// const promise = this.props.onSubmit(activeStep, form); // const promise = this.props.onSubmit(activeStep, form);

View File

@ -10,11 +10,14 @@ import messages from '../MultiFactorAuth.intl.json';
import styles from './key-form.scss'; import styles from './key-form.scss';
export default function KeyForm() { export default function KeyForm({secret, qrCodeSrc}: {
const key = '123 123 52354 1234'; secret: string,
qrCodeSrc: string
}) {
const formattedSecret = formatSecret(secret);
return ( return (
<div className={profileForm.formBody} key="step2"> <div className={profileForm.formBody}>
<div className={profileForm.formRow}> <div className={profileForm.formRow}>
<p className={profileForm.description}> <p className={profileForm.description}>
<Message {...messages.scanQrCode} /> <Message {...messages.scanQrCode} />
@ -23,22 +26,22 @@ export default function KeyForm() {
<div className={profileForm.formRow}> <div className={profileForm.formRow}>
<div className={styles.qrCode}> <div className={styles.qrCode}>
<img src="//placekitten.com/g/242/242" alt={key} /> <img src={qrCodeSrc} alt={secret} />
</div> </div>
</div> </div>
<div className={profileForm.formRow}> <div className={profileForm.formRow}>
<p className={classNames(styles.manualDescription, profileForm.description)}> <p className={classNames(styles.manualDescription, profileForm.description)}>
<div className={styles.or}> <span className={styles.or}>
<Message {...messages.or} /> <Message {...messages.or} />
</div> </span>
<Message {...messages.enterKeyManually} /> <Message {...messages.enterKeyManually} />
</p> </p>
</div> </div>
<div className={profileForm.formRow}> <div className={profileForm.formRow}>
<div className={styles.key}> <div className={styles.key}>
{key} {formattedSecret}
</div> </div>
</div> </div>
@ -50,3 +53,7 @@ export default function KeyForm() {
</div> </div>
); );
} }
function formatSecret(secret: string): string {
return (secret.match(/.{1,4}/g) || []).join(' ');
}

9
src/services/api/mfa.js Normal file
View File

@ -0,0 +1,9 @@
// @flow
import request from 'services/request';
import type { Resp } from 'services/request';
export default {
getSecret(): Promise<Resp<{qr: string, secret: string, uri: string}>> {
return request.get('/api/two-factor-auth');
}
};

View File

@ -1,5 +1,7 @@
import request from './request'; // @flow
import InternalServerError from './InternalServerError'; export { default } from './request';
export type { Resp } from './request';
export { default as InternalServerError } from './InternalServerError';
/** /**
* Usage: Query<'requeired'|'keys'|'names'> * Usage: Query<'requeired'|'keys'|'names'>
@ -9,7 +11,3 @@ export type Query<T: string> = {
get: (key: T) => ?string, get: (key: T) => ?string,
set: (key: T, value: any) => void, set: (key: T, value: any) => void,
}; };
export default request;
export { InternalServerError };

View File

@ -1,17 +1,28 @@
// @flow
import PromiseMiddlewareLayer from './PromiseMiddlewareLayer'; import PromiseMiddlewareLayer from './PromiseMiddlewareLayer';
import InternalServerError from './InternalServerError'; import InternalServerError from './InternalServerError';
const middlewareLayer = new PromiseMiddlewareLayer(); const middlewareLayer = new PromiseMiddlewareLayer();
export type Resp<T> = {
originalResponse: Response
} & T;
type Middleware = {
before?: () => Promise<*>,
after?: () => Promise<*>,
catch?: () => Promise<*>
};
export default { export default {
/** /**
* @param {string} url * @param {string} url
* @param {object} data - request data * @param {object} [data] - request data
* @param {object} options - additional options for fetch or middlewares * @param {object} [options] - additional options for fetch or middlewares
* *
* @return {Promise} * @return {Promise}
*/ */
post(url, data, options = {}) { post<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
return doFetch(url, { return doFetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -24,12 +35,12 @@ export default {
/** /**
* @param {string} url * @param {string} url
* @param {object} data - request data * @param {object} [data] - request data
* @param {object} options - additional options for fetch or middlewares * @param {object} [options] - additional options for fetch or middlewares
* *
* @return {Promise} * @return {Promise}
*/ */
get(url, data, options = {}) { get<T>(url: string, data?: Object, options: Object = {}): Promise<Resp<T>> {
if (typeof data === 'object' && Object.keys(data).length) { if (typeof data === 'object' && Object.keys(data).length) {
const separator = url.indexOf('?') === -1 ? '?' : '&'; const separator = url.indexOf('?') === -1 ? '?' : '&';
url += separator + buildQuery(data); url += separator + buildQuery(data);
@ -59,13 +70,15 @@ export default {
* get response and callback to restart request as an arguments and should * get response and callback to restart request as an arguments and should
* return a Promise that resolves to the new response. * return a Promise that resolves to the new response.
*/ */
addMiddleware(middleware) { addMiddleware(middleware: Middleware) {
middlewareLayer.add(middleware); middlewareLayer.add(middleware);
} }
}; };
const checkStatus = (resp) => Promise[resp.status >= 200 && resp.status < 300 ? 'resolve' : 'reject'](resp); const checkStatus = (resp) => resp.status >= 200 && resp.status < 300
? Promise.resolve(resp)
: Promise.reject(resp);
const toJSON = (resp = {}) => { const toJSON = (resp = {}) => {
if (!resp.json) { if (!resp.json) {
// e.g. 'TypeError: Failed to fetch' due to CORS // e.g. 'TypeError: Failed to fetch' due to CORS
@ -87,7 +100,9 @@ const rejectWithJSON = (resp) => toJSON(resp).then((resp) => {
throw resp; throw resp;
}); });
const handleResponseSuccess = (resp) => Promise[resp.success || typeof resp.success === 'undefined' ? 'resolve' : 'reject'](resp); const handleResponseSuccess = (resp) => resp.success || typeof resp.success === 'undefined'
? Promise.resolve(resp)
: Promise.reject(resp);
function doFetch(url, options = {}) { function doFetch(url, options = {}) {
// NOTE: we are wrapping fetch, because it is returning // NOTE: we are wrapping fetch, because it is returning
@ -136,7 +151,7 @@ function convertQueryValue(value) {
* *
* @return {string} * @return {string}
*/ */
function buildQuery(data = {}) { function buildQuery(data: Object = {}): string {
return Object.keys(data) return Object.keys(data)
.map( .map(
(keyName) => (keyName) =>