2017-08-23 00:09:08 +05:30
|
|
|
// @flow
|
2017-05-26 00:41:57 +05:30
|
|
|
import { browserHistory } from 'services/history';
|
2016-03-02 02:06:14 +05:30
|
|
|
|
2016-12-07 02:38:51 +05:30
|
|
|
import logger from 'services/logger';
|
2017-04-12 00:48:27 +05:30
|
|
|
import localStorage from 'services/localStorage';
|
2016-12-07 02:38:51 +05:30
|
|
|
|
2016-03-02 02:06:14 +05:30
|
|
|
import RegisterState from './RegisterState';
|
|
|
|
import LoginState from './LoginState';
|
|
|
|
import OAuthState from './OAuthState';
|
|
|
|
import ForgotPasswordState from './ForgotPasswordState';
|
2016-05-15 02:23:58 +05:30
|
|
|
import RecoverPasswordState from './RecoverPasswordState';
|
2016-06-05 17:36:14 +05:30
|
|
|
import ActivationState from './ActivationState';
|
2016-08-27 15:49:02 +05:30
|
|
|
import CompleteState from './CompleteState';
|
2016-05-23 00:28:43 +05:30
|
|
|
import ResendActivationState from './ResendActivationState';
|
2017-06-08 01:52:51 +05:30
|
|
|
import type AbstractState from './AbstractState';
|
|
|
|
|
2017-08-23 00:09:08 +05:30
|
|
|
type Request = {
|
|
|
|
path: string,
|
|
|
|
query: URLSearchParams,
|
|
|
|
params: Object
|
2017-06-08 01:52:51 +05:30
|
|
|
};
|
2017-09-09 20:34:26 +05:30
|
|
|
|
|
|
|
// TODO: temporary added to improve typing without major refactoring
|
|
|
|
type ActionId =
|
|
|
|
| 'updateUser'
|
|
|
|
| 'authenticate'
|
|
|
|
| 'logout'
|
|
|
|
| 'goBack'
|
|
|
|
| 'redirect'
|
|
|
|
| 'login'
|
|
|
|
| 'acceptRules'
|
|
|
|
| 'forgotPassword'
|
|
|
|
| 'recoverPassword'
|
|
|
|
| 'register'
|
|
|
|
| 'activate'
|
|
|
|
| 'resendActivation'
|
|
|
|
| 'contactUs'
|
|
|
|
| 'setLogin'
|
|
|
|
| 'setAccountSwitcher'
|
|
|
|
| 'setErrors'
|
|
|
|
| 'clearErrors'
|
|
|
|
| 'oAuthValidate'
|
|
|
|
| 'oAuthComplete'
|
|
|
|
| 'setClient'
|
|
|
|
| 'resetOAuth'
|
|
|
|
| 'resetAuth'
|
|
|
|
| 'setOAuthRequest'
|
|
|
|
| 'setOAuthCode'
|
|
|
|
| 'requirePermissionsAccept'
|
|
|
|
| 'setScopes'
|
|
|
|
| 'setLoadingState';
|
|
|
|
|
2017-08-23 00:09:08 +05:30
|
|
|
export interface AuthContext {
|
2017-12-31 00:34:31 +05:30
|
|
|
run(actionId: ActionId, payload?: mixed): *;
|
2017-09-09 20:34:26 +05:30
|
|
|
setState(newState: AbstractState): Promise<*> | void;
|
2017-08-23 00:09:08 +05:30
|
|
|
getState(): Object;
|
|
|
|
navigate(route: string): void;
|
|
|
|
getRequest(): Request;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class AuthFlow implements AuthContext {
|
2017-12-31 00:34:31 +05:30
|
|
|
actions: {[key: string]: (mixed) => Object};
|
2017-08-23 00:09:08 +05:30
|
|
|
state: AbstractState;
|
|
|
|
prevState: AbstractState;
|
|
|
|
/**
|
|
|
|
* A callback from router, that allows to replace (perform redirect) route
|
|
|
|
* during route transition
|
|
|
|
*/
|
|
|
|
replace: ?(string) => void;
|
|
|
|
onReady: Function;
|
|
|
|
navigate: Function;
|
|
|
|
currentRequest: Request;
|
|
|
|
dispatch: (action: Object) => void;
|
|
|
|
getState: () => Object;
|
|
|
|
|
|
|
|
constructor(actions: {[key: string]: Function}) {
|
2016-04-12 09:19:58 +05:30
|
|
|
if (typeof actions !== 'object') {
|
|
|
|
throw new Error('AuthFlow requires an actions object');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.actions = actions;
|
|
|
|
|
|
|
|
if (Object.freeze) {
|
|
|
|
Object.freeze(this.actions);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-23 00:09:08 +05:30
|
|
|
setStore(store: *) {
|
2016-08-27 20:06:33 +05:30
|
|
|
/**
|
|
|
|
* @param {string} route
|
|
|
|
* @param {object} options
|
|
|
|
* @param {object} options.replace
|
|
|
|
*/
|
2017-08-23 00:09:08 +05:30
|
|
|
this.navigate = (route: string, options: {replace?: bool} = {}) => {
|
2016-08-07 19:24:59 +05:30
|
|
|
if (this.getRequest().path !== route) {
|
|
|
|
this.currentRequest = {
|
2017-08-23 00:09:08 +05:30
|
|
|
path: route,
|
|
|
|
params: {},
|
|
|
|
query: new URLSearchParams()
|
2016-08-07 19:24:59 +05:30
|
|
|
};
|
2016-06-15 11:31:41 +05:30
|
|
|
|
2016-03-02 02:06:14 +05:30
|
|
|
if (this.replace) {
|
|
|
|
this.replace(route);
|
|
|
|
}
|
2017-05-09 01:04:50 +05:30
|
|
|
|
|
|
|
browserHistory[options.replace ? 'replace' : 'push'](route);
|
2016-03-02 02:06:14 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
this.replace = null;
|
|
|
|
};
|
|
|
|
|
|
|
|
this.getState = store.getState.bind(store);
|
|
|
|
this.dispatch = store.dispatch.bind(store);
|
|
|
|
}
|
|
|
|
|
2017-08-23 00:09:08 +05:30
|
|
|
resolve(payload: Object = {}) {
|
2016-03-02 02:06:14 +05:30
|
|
|
this.state.resolve(this, payload);
|
|
|
|
}
|
|
|
|
|
2017-08-23 00:09:08 +05:30
|
|
|
reject(payload: Object = {}) {
|
2016-03-02 02:06:14 +05:30
|
|
|
this.state.reject(this, payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
goBack() {
|
|
|
|
this.state.goBack(this);
|
|
|
|
}
|
|
|
|
|
2017-12-31 00:34:31 +05:30
|
|
|
run(actionId: ActionId, payload?: mixed): Promise<any> {
|
|
|
|
const action = this.actions[actionId];
|
|
|
|
|
|
|
|
if (!action) {
|
2016-03-02 02:06:14 +05:30
|
|
|
throw new Error(`Action ${actionId} does not exists`);
|
|
|
|
}
|
|
|
|
|
2017-12-31 00:34:31 +05:30
|
|
|
return Promise.resolve(
|
|
|
|
this.dispatch(
|
|
|
|
action(payload)
|
|
|
|
)
|
|
|
|
);
|
2016-03-02 02:06:14 +05:30
|
|
|
}
|
|
|
|
|
2017-08-23 00:09:08 +05:30
|
|
|
setState(state: AbstractState) {
|
2016-03-02 02:06:14 +05:30
|
|
|
if (!state) {
|
|
|
|
throw new Error('State is required');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.state && this.state.leave(this);
|
2016-06-05 17:36:14 +05:30
|
|
|
this.prevState = this.state;
|
2016-03-02 02:06:14 +05:30
|
|
|
this.state = state;
|
2016-06-02 23:16:49 +05:30
|
|
|
const resp = this.state.enter(this);
|
|
|
|
|
|
|
|
if (resp && resp.then) {
|
|
|
|
// this is a state with an async enter phase
|
|
|
|
// block route components from mounting, till promise will be resolved
|
2016-06-04 00:40:47 +05:30
|
|
|
if (this.onReady) {
|
|
|
|
const callback = this.onReady;
|
|
|
|
this.onReady = () => {};
|
2017-03-02 11:28:33 +05:30
|
|
|
return resp.then(callback, (err) => err || logger.warn('State transition error', err));
|
2016-06-04 00:40:47 +05:30
|
|
|
}
|
2016-06-04 14:47:06 +05:30
|
|
|
|
|
|
|
return resp;
|
2016-06-02 23:16:49 +05:30
|
|
|
}
|
2016-03-02 02:06:14 +05:30
|
|
|
}
|
|
|
|
|
2016-08-07 19:24:59 +05:30
|
|
|
/**
|
|
|
|
* @return {object} - current request object
|
|
|
|
*/
|
|
|
|
getRequest() {
|
2016-08-07 20:19:26 +05:30
|
|
|
return {
|
|
|
|
path: '',
|
2017-05-26 00:41:57 +05:30
|
|
|
query: new URLSearchParams(),
|
2016-08-07 20:19:26 +05:30
|
|
|
params: {},
|
|
|
|
...this.currentRequest
|
|
|
|
};
|
2016-06-15 11:32:12 +05:30
|
|
|
}
|
|
|
|
|
2016-06-04 00:40:47 +05:30
|
|
|
/**
|
|
|
|
* This should be called from onEnter prop of react-router Route component
|
|
|
|
*
|
2016-08-07 19:24:59 +05:30
|
|
|
* @param {object} request
|
|
|
|
* @param {string} request.path
|
|
|
|
* @param {object} request.params
|
2017-07-12 01:26:07 +05:30
|
|
|
* @param {URLSearchParams} request.query
|
2016-06-04 00:40:47 +05:30
|
|
|
* @param {function} replace
|
|
|
|
* @param {function} [callback = function() {}] - an optional callback function to be called, when state will be stabilized
|
|
|
|
* (state's enter function's promise resolved)
|
|
|
|
*/
|
2017-08-23 00:09:08 +05:30
|
|
|
handleRequest(request: Request, replace: Function, callback: Function = function() {}) {
|
2016-12-07 02:38:51 +05:30
|
|
|
const {path} = request;
|
2016-03-02 02:06:14 +05:30
|
|
|
this.replace = replace;
|
2016-06-02 23:16:49 +05:30
|
|
|
this.onReady = callback;
|
2016-03-02 02:06:14 +05:30
|
|
|
|
2016-08-07 19:24:59 +05:30
|
|
|
if (!path) {
|
|
|
|
throw new Error('The request.path is required');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.getRequest().path === path) {
|
2016-06-15 11:31:41 +05:30
|
|
|
// we are already handling this path
|
|
|
|
this.onReady();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-08-07 19:24:59 +05:30
|
|
|
this.currentRequest = request;
|
2016-06-15 11:31:41 +05:30
|
|
|
|
2016-08-27 15:49:02 +05:30
|
|
|
if (this.restoreOAuthState()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-07-28 01:15:50 +05:30
|
|
|
switch (path) {
|
2016-03-02 02:06:14 +05:30
|
|
|
case '/register':
|
|
|
|
this.setState(new RegisterState());
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '/forgot-password':
|
|
|
|
this.setState(new ForgotPasswordState());
|
|
|
|
break;
|
|
|
|
|
2016-05-23 00:28:43 +05:30
|
|
|
case '/resend-activation':
|
|
|
|
this.setState(new ResendActivationState());
|
|
|
|
break;
|
|
|
|
|
2016-03-13 14:32:24 +05:30
|
|
|
case '/':
|
2016-03-02 02:06:14 +05:30
|
|
|
case '/login':
|
|
|
|
case '/password':
|
2017-08-23 00:09:08 +05:30
|
|
|
case '/mfa':
|
2016-08-03 00:29:29 +05:30
|
|
|
case '/accept-rules':
|
2016-03-02 02:06:14 +05:30
|
|
|
case '/oauth/permissions':
|
2016-03-15 12:06:13 +05:30
|
|
|
case '/oauth/finish':
|
2016-11-13 20:17:56 +05:30
|
|
|
case '/oauth/choose-account':
|
2016-03-02 02:06:14 +05:30
|
|
|
this.setState(new LoginState());
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2016-05-28 03:54:22 +05:30
|
|
|
switch (path.replace(/(.)\/.+/, '$1')) { // use only first part of an url
|
2016-08-07 20:19:26 +05:30
|
|
|
case '/oauth2':
|
|
|
|
this.setState(new OAuthState());
|
|
|
|
break;
|
2016-06-05 17:36:14 +05:30
|
|
|
case '/activation':
|
|
|
|
this.setState(new ActivationState());
|
|
|
|
break;
|
2016-05-28 03:54:22 +05:30
|
|
|
case '/recover-password':
|
|
|
|
this.setState(new RecoverPasswordState());
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2016-08-07 19:24:59 +05:30
|
|
|
replace('/404');
|
|
|
|
break;
|
2016-05-28 03:54:22 +05:30
|
|
|
}
|
2016-03-02 02:06:14 +05:30
|
|
|
}
|
2016-06-02 23:16:49 +05:30
|
|
|
|
|
|
|
this.onReady();
|
2016-03-02 02:06:14 +05:30
|
|
|
}
|
2016-08-12 00:50:14 +05:30
|
|
|
|
|
|
|
/**
|
2016-08-27 15:49:02 +05:30
|
|
|
* Tries to restore last oauth request, if it was stored in localStorage
|
|
|
|
* in last 2 hours
|
2016-08-12 00:50:14 +05:30
|
|
|
* @api private
|
2016-08-27 15:49:02 +05:30
|
|
|
*
|
|
|
|
* @return {bool} - whether oauth state is being restored
|
2016-08-12 00:50:14 +05:30
|
|
|
*/
|
|
|
|
restoreOAuthState() {
|
2016-11-19 20:11:15 +05:30
|
|
|
if (/^\/(register|oauth2)/.test(this.getRequest().path)) {
|
|
|
|
// allow register or the new oauth requests
|
2016-10-25 11:31:51 +05:30
|
|
|
return;
|
|
|
|
}
|
2016-10-25 05:10:05 +05:30
|
|
|
|
2016-08-12 00:50:14 +05:30
|
|
|
try {
|
|
|
|
const data = JSON.parse(localStorage.getItem('oauthData'));
|
2016-08-27 15:49:02 +05:30
|
|
|
const expirationTime = 2 * 60 * 60 * 1000; // 2h
|
|
|
|
|
|
|
|
if (Date.now() - data.timestamp < expirationTime) {
|
|
|
|
this.run('oAuthValidate', data.payload)
|
2016-10-25 12:02:50 +05:30
|
|
|
.then(() => this.setState(new CompleteState()))
|
|
|
|
.then(() => this.onReady());
|
2016-08-12 00:50:14 +05:30
|
|
|
|
2016-08-27 15:49:02 +05:30
|
|
|
return true;
|
2016-08-12 00:50:14 +05:30
|
|
|
}
|
|
|
|
} catch (err) {/* bad luck :( */}
|
2016-08-27 15:49:02 +05:30
|
|
|
|
|
|
|
return false;
|
2016-08-12 00:50:14 +05:30
|
|
|
}
|
2016-03-02 02:06:14 +05:30
|
|
|
}
|