accounts-frontend/src/services/authFlow/AuthFlow.js

283 lines
7.8 KiB
JavaScript
Raw Normal View History

2017-08-23 00:09:08 +05:30
// @flow
import { browserHistory } from 'services/history';
2016-03-02 02:06:14 +05:30
import logger from 'services/logger';
import localStorage from 'services/localStorage';
2016-03-02 02:06:14 +05:30
import RegisterState from './RegisterState';
import LoginState from './LoginState';
import OAuthState from './OAuthState';
import ForgotPasswordState from './ForgotPasswordState';
import RecoverPasswordState from './RecoverPasswordState';
import ActivationState from './ActivationState';
2016-08-27 15:49:02 +05:30
import CompleteState from './CompleteState';
import ResendActivationState from './ResendActivationState';
import type AbstractState from './AbstractState';
2017-08-23 00:09:08 +05:30
type Request = {
path: string,
query: URLSearchParams,
params: Object
};
// 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 {
run(actionId: ActionId, payload?: mixed): *;
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 {
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}) {
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: *) {
/**
* @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} = {}) => {
if (this.getRequest().path !== route) {
this.currentRequest = {
2017-08-23 00:09:08 +05:30
path: route,
params: {},
query: new URLSearchParams()
};
2016-03-02 02:06:14 +05:30
if (this.replace) {
this.replace(route);
}
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);
}
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`);
}
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);
this.prevState = this.state;
2016-03-02 02:06:14 +05:30
this.state = state;
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
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));
}
return resp;
}
2016-03-02 02:06:14 +05:30
}
/**
* @return {object} - current request object
*/
getRequest() {
return {
path: '',
query: new URLSearchParams(),
params: {},
...this.currentRequest
};
}
/**
* This should be called from onEnter prop of react-router Route component
*
* @param {object} request
* @param {string} request.path
* @param {object} request.params
* @param {URLSearchParams} request.query
* @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() {}) {
const {path} = request;
2016-03-02 02:06:14 +05:30
this.replace = replace;
this.onReady = callback;
2016-03-02 02:06:14 +05:30
if (!path) {
throw new Error('The request.path is required');
}
if (this.getRequest().path === path) {
// we are already handling this path
this.onReady();
return;
}
this.currentRequest = request;
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;
case '/resend-activation':
this.setState(new ResendActivationState());
break;
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':
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
case '/oauth2':
this.setState(new OAuthState());
break;
case '/activation':
this.setState(new ActivationState());
break;
2016-05-28 03:54:22 +05:30
case '/recover-password':
this.setState(new RecoverPasswordState());
break;
default:
replace('/404');
break;
2016-05-28 03:54:22 +05:30
}
2016-03-02 02:06:14 +05:30
}
this.onReady();
2016-03-02 02:06: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
* @api private
2016-08-27 15:49:02 +05:30
*
* @return {bool} - whether oauth state is being restored
*/
restoreOAuthState() {
if (/^\/(register|oauth2)/.test(this.getRequest().path)) {
// allow register or the new oauth requests
return;
}
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)
.then(() => this.setState(new CompleteState()))
.then(() => this.onReady());
2016-08-27 15:49:02 +05:30
return true;
}
} catch (err) {/* bad luck :( */}
2016-08-27 15:49:02 +05:30
return false;
}
2016-03-02 02:06:14 +05:30
}