Remove SharedWorker in favor of cookies
Safari and Internet Explorer don't implement the SharedWorker API which causes the referenced bug. We're not using the SharedWorker API for anything other than storing the keystone token. This change removes the SharedWorker code altogether, and instead stores the token in a standard cookie. New dependency: - react-cookie (MIT) https://github.com/thereactivestack/react-cookie Change-Id: Ieebcd946b59cb4afb696e12eabf75dec96aeab11 Closes-Bug: 1647590
This commit is contained in:
parent
c107619eda
commit
9c9061c53b
|
@ -60,7 +60,6 @@
|
|||
"phantomjs-prebuilt": "~2.1.7",
|
||||
"react-addons-test-utils": "~15.0.2",
|
||||
"react-intl-po": "^1.1.0",
|
||||
"shared-worker-loader": "~0.1.0",
|
||||
"style-loader": "~0.13.1",
|
||||
"url-loader": "~0.5.7",
|
||||
"webpack": "~1.13.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import KeystoneApiService from '../../js/services/KeystoneApiService';
|
||||
import LoginActions from '../../js/actions/LoginActions';
|
||||
import TempStorage from '../../js/services/TempStorage.js';
|
||||
import cookie from 'react-cookie';
|
||||
|
||||
let mockKeystoneAccess = {
|
||||
token: {
|
||||
|
@ -19,17 +19,17 @@ describe('LoginActions', () => {
|
|||
});
|
||||
|
||||
xit('creates action to login user with keystoneAccess response', () => {
|
||||
spyOn(TempStorage, 'setItem');
|
||||
spyOn(cookie, 'save');
|
||||
LoginActions.loginUser(mockKeystoneAccess);
|
||||
expect(TempStorage.setItem).toHaveBeenCalledWith(
|
||||
expect(cookie.save).toHaveBeenCalledWith(
|
||||
'keystoneAuthTokenId',
|
||||
mockKeystoneAccess.token.id
|
||||
);
|
||||
});
|
||||
|
||||
xit('creates action to logout user', () => {
|
||||
spyOn(TempStorage, 'removeItem');
|
||||
spyOn(cookie, 'remove');
|
||||
LoginActions.logoutUser();
|
||||
expect(TempStorage.removeItem).toHaveBeenCalled();
|
||||
expect(cookie.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import TempStorage from '../../js/services/TempStorage.js';
|
||||
import TempStorageWorker from '../../js/workers/TempStorageWorker';
|
||||
|
||||
describe('TempStorage', () => {
|
||||
describe('.getItem', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.removeItem('someKey');
|
||||
});
|
||||
|
||||
it('returns ``null`` if no value has been set yet.', () => {
|
||||
sessionStorage.removeItem('someKey');
|
||||
expect(TempStorage.getItem('someKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns the value from sessionStorage if it is set', () => {
|
||||
sessionStorage.setItem('someKey', 'someValue');
|
||||
expect(TempStorage.getItem('someKey')).toEqual('someValue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('worker updates', () => {
|
||||
let worker;
|
||||
|
||||
if(window && window.SharedWorker) {
|
||||
worker = new TempStorageWorker();
|
||||
worker.port.start();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.removeItem('someKey');
|
||||
});
|
||||
|
||||
it('update the sessionStorage items as well', () => {
|
||||
expect(TempStorage.getItem('someKey')).toBeNull();
|
||||
worker.port.postMessage({someKey: 'updated'});
|
||||
setTimeout(() => {
|
||||
expect(TempStorage.getItem('someKey')).toEqual('updated');
|
||||
expect(sessionStorage.getItem('someKey')).toEqual('updated');
|
||||
}, 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('```.setItem```', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.removeItem('someKey');
|
||||
});
|
||||
|
||||
it('updates the worker as well as sessionStorage', () => {
|
||||
expect(TempStorage.getItem('someKey')).toBeNull();
|
||||
TempStorage.setItem('someKey', 'newVal');
|
||||
setTimeout(() => {
|
||||
expect(sessionStorage.getItem('someKey')).toBe('newVal');
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,26 +1,26 @@
|
|||
import { browserHistory } from 'react-router';
|
||||
import { Map, fromJS } from 'immutable';
|
||||
|
||||
import TempStorage from '../services/TempStorage.js';
|
||||
import KeystoneApiErrorHandler from '../services/KeystoneApiErrorHandler';
|
||||
import KeystoneApiService from '../services/KeystoneApiService';
|
||||
import LoginConstants from '../constants/LoginConstants';
|
||||
import ZaqarWebSocketService from '../services/ZaqarWebSocketService';
|
||||
import logger from '../services/logger';
|
||||
import cookie from 'react-cookie';
|
||||
|
||||
export default {
|
||||
authenticateUserViaToken(keystoneAuthTokenId, nextPath) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(this.userAuthStarted());
|
||||
KeystoneApiService.authenticateUserViaToken(keystoneAuthTokenId).then((response) => {
|
||||
TempStorage.setItem('keystoneAuthTokenId', response.access.token.id);
|
||||
cookie.save('keystoneAuthTokenId', response.access.token.id);
|
||||
dispatch(this.userAuthSuccess(response.access));
|
||||
ZaqarWebSocketService.init(getState, dispatch);
|
||||
browserHistory.push(nextPath);
|
||||
}).catch((error) => {
|
||||
logger.error('Error in LoginActions.authenticateUserViaToken', error.stack || error);
|
||||
let errorHandler = new KeystoneApiErrorHandler(error);
|
||||
TempStorage.removeItem('keystoneAuthTokenId');
|
||||
cookie.remove('keystoneAuthTokenId');
|
||||
browserHistory.push({pathname: '/login', query: { nextPath: nextPath }});
|
||||
dispatch(this.userAuthFailure(errorHandler.errors));
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ export default {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(this.userAuthStarted());
|
||||
KeystoneApiService.authenticateUser(formData.username, formData.password).then((response) => {
|
||||
TempStorage.setItem('keystoneAuthTokenId', response.access.token.id);
|
||||
cookie.save('keystoneAuthTokenId', response.access.token.id);
|
||||
dispatch(this.userAuthSuccess(response.access));
|
||||
ZaqarWebSocketService.init(getState, dispatch);
|
||||
browserHistory.push(nextPath);
|
||||
|
@ -70,7 +70,7 @@ export default {
|
|||
logoutUser() {
|
||||
return dispatch => {
|
||||
browserHistory.push('/login');
|
||||
TempStorage.removeItem('keystoneAuthTokenId');
|
||||
cookie.remove('keystoneAuthTokenId');
|
||||
ZaqarWebSocketService.close();
|
||||
dispatch(this.logoutUserSuccess());
|
||||
};
|
||||
|
|
171
src/js/index.js
171
src/js/index.js
|
@ -5,6 +5,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, Redirect } from 'react-router';
|
||||
import { browserHistory } from 'react-router';
|
||||
import cookie from 'react-cookie';
|
||||
|
||||
import App from './components/App';
|
||||
import AuthenticatedContent from './components/AuthenticatedContent';
|
||||
|
@ -36,110 +37,104 @@ import RoleDetail from './components/roles/RoleDetail';
|
|||
import RoleNetworkConfig from './components/roles/RoleNetworkConfig';
|
||||
import RoleParameters from './components/roles/RoleParameters';
|
||||
import RoleServices from './components/roles/RoleServices';
|
||||
import TempStorage from './services/TempStorage.js';
|
||||
import store from './store';
|
||||
|
||||
import '../less/base.less';
|
||||
|
||||
|
||||
TempStorage.initialized.then(() => {
|
||||
/**
|
||||
* @function checkAuth
|
||||
* If user is not logged in, check if there is an auth token in TempStorage
|
||||
* If there is, try to login with this token, else redirect to Login
|
||||
*/
|
||||
function checkAuth(nextState, replace) {
|
||||
if (!store.getState().login.hasIn(['keystoneAccess','user'])) {
|
||||
const keystoneAuthTokenId = TempStorage.getItem('keystoneAuthTokenId');
|
||||
if (keystoneAuthTokenId) {
|
||||
const nextPath = nextState.location.pathname +
|
||||
nextState.location.search || '/';
|
||||
store.dispatch(LoginActions.authenticateUserViaToken(keystoneAuthTokenId, nextPath));
|
||||
} else {
|
||||
replace({ pathname: '/login',
|
||||
query: { nextPath: nextState.location.pathname + nextState.location.search } });
|
||||
}
|
||||
/**
|
||||
* @function checkAuth
|
||||
* If user is not logged in, check if there is an auth token in a cookie
|
||||
* If there is, try to login with this token, else redirect to Login
|
||||
*/
|
||||
function checkAuth(nextState, replace) {
|
||||
if (!store.getState().login.hasIn(['keystoneAccess','user'])) {
|
||||
const keystoneAuthTokenId = cookie.load('keystoneAuthTokenId');
|
||||
if (keystoneAuthTokenId) {
|
||||
const nextPath = nextState.location.pathname +
|
||||
nextState.location.search || '/';
|
||||
store.dispatch(LoginActions.authenticateUserViaToken(keystoneAuthTokenId, nextPath));
|
||||
} else {
|
||||
replace({ pathname: '/login',
|
||||
query: { nextPath: nextState.location.pathname + nextState.location.search } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkRunningDeployment(nextState, replace) {
|
||||
const state = store.getState();
|
||||
let currentPlanName = state.currentPlan.currentPlanName;
|
||||
if(getCurrentStackDeploymentInProgress(state)) {
|
||||
store.dispatch(NotificationActions.notify({
|
||||
title: 'Not allowed',
|
||||
message: `A deployment for the plan ${currentPlanName} is already in progress.`,
|
||||
type: 'warning'
|
||||
}));
|
||||
// TODO(flfuchs): Redirect to deployment status modal instead of DeploymentPlan
|
||||
// page (in separate patch).
|
||||
replace('/deployment-plan/');
|
||||
}
|
||||
function checkRunningDeployment(nextState, replace) {
|
||||
const state = store.getState();
|
||||
let currentPlanName = state.currentPlan.currentPlanName;
|
||||
if(getCurrentStackDeploymentInProgress(state)) {
|
||||
store.dispatch(NotificationActions.notify({
|
||||
title: 'Not allowed',
|
||||
message: `A deployment for the plan ${currentPlanName} is already in progress.`,
|
||||
type: 'warning'
|
||||
}));
|
||||
// TODO(flfuchs): Redirect to deployment status modal instead of DeploymentPlan
|
||||
// page (in separate patch).
|
||||
replace('/deployment-plan/');
|
||||
}
|
||||
}
|
||||
|
||||
let routes = (
|
||||
<Route>
|
||||
<Redirect from="/" to="/deployment-plan"/>
|
||||
<Route path="/" component={App}>
|
||||
<Route component={UserAuthenticator} onEnter={checkAuth}>
|
||||
<Route component={AuthenticatedContent}>
|
||||
let routes = (
|
||||
<Route>
|
||||
<Redirect from="/" to="/deployment-plan"/>
|
||||
<Route path="/" component={App}>
|
||||
<Route component={UserAuthenticator} onEnter={checkAuth}>
|
||||
<Route component={AuthenticatedContent}>
|
||||
|
||||
<Route path="deployment-plan" component={DeploymentPlan}>
|
||||
<Redirect from="configuration" to="configuration/environment"/>
|
||||
<Route path="configuration"
|
||||
component={DeploymentConfiguration}
|
||||
onEnter={checkRunningDeployment}>
|
||||
<Route path="environment" component={EnvironmentConfiguration}/>
|
||||
<Route path="parameters" component={Parameters}/>
|
||||
</Route>
|
||||
<Route path=":roleIdentifier/assign-nodes"
|
||||
component={NodesAssignment}
|
||||
onEnter={checkRunningDeployment}/>
|
||||
<Redirect from="roles/:roleIdentifier" to="roles/:roleIdentifier/parameters"/>
|
||||
<Route path="roles/:roleIdentifier"
|
||||
component={RoleDetail}
|
||||
onEnter={checkRunningDeployment}>
|
||||
<Route path="parameters" component={RoleParameters}/>
|
||||
<Route path="services" component={RoleServices}/>
|
||||
<Route path="network-configuration" component={RoleNetworkConfig}/>
|
||||
</Route>
|
||||
<Route path="deployment-detail"
|
||||
component={DeploymentDetail}/>
|
||||
<Route path="deployment-plan" component={DeploymentPlan}>
|
||||
<Redirect from="configuration" to="configuration/environment"/>
|
||||
<Route path="configuration"
|
||||
component={DeploymentConfiguration}
|
||||
onEnter={checkRunningDeployment}>
|
||||
<Route path="environment" component={EnvironmentConfiguration}/>
|
||||
<Route path="parameters" component={Parameters}/>
|
||||
</Route>
|
||||
|
||||
<Redirect from="nodes" to="nodes/registered"/>
|
||||
<Route path="nodes" component={Nodes}>
|
||||
<Route path="registered" component={RegisteredNodesTabPane}>
|
||||
<Route path="register" component={RegisterNodesDialog}/>
|
||||
</Route>
|
||||
<Route path="deployed" component={DeployedNodesTabPane}/>
|
||||
<Route path="maintenance" component={MaintenanceNodesTabPane}/>
|
||||
<Route path=":roleIdentifier/assign-nodes"
|
||||
component={NodesAssignment}
|
||||
onEnter={checkRunningDeployment}/>
|
||||
<Redirect from="roles/:roleIdentifier" to="roles/:roleIdentifier/parameters"/>
|
||||
<Route path="roles/:roleIdentifier"
|
||||
component={RoleDetail}
|
||||
onEnter={checkRunningDeployment}>
|
||||
<Route path="parameters" component={RoleParameters}/>
|
||||
<Route path="services" component={RoleServices}/>
|
||||
<Route path="network-configuration" component={RoleNetworkConfig}/>
|
||||
</Route>
|
||||
<Route path="deployment-detail"
|
||||
component={DeploymentDetail}/>
|
||||
</Route>
|
||||
|
||||
<Redirect from="plans" to="plans/list"/>
|
||||
<Route path="plans" component={Plans}>
|
||||
<Route path="list" component={ListPlans}>
|
||||
<Route path="/plans/new" component={NewPlan}/>
|
||||
<Route path="/plans/:planName/delete" component={DeletePlan}/>
|
||||
<Route path="/plans/:planName/edit" component={EditPlan}/>
|
||||
</Route>
|
||||
<Redirect from="nodes" to="nodes/registered"/>
|
||||
<Route path="nodes" component={Nodes}>
|
||||
<Route path="registered" component={RegisteredNodesTabPane}>
|
||||
<Route path="register" component={RegisterNodesDialog}/>
|
||||
</Route>
|
||||
<Route path="deployed" component={DeployedNodesTabPane}/>
|
||||
<Route path="maintenance" component={MaintenanceNodesTabPane}/>
|
||||
</Route>
|
||||
|
||||
<Redirect from="plans" to="plans/list"/>
|
||||
<Route path="plans" component={Plans}>
|
||||
<Route path="list" component={ListPlans}>
|
||||
<Route path="/plans/new" component={NewPlan}/>
|
||||
<Route path="/plans/:planName/delete" component={DeletePlan}/>
|
||||
<Route path="/plans/:planName/edit" component={EditPlan}/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="login" component={Login}/>
|
||||
</Route>
|
||||
<Route path="login" component={Login}/>
|
||||
</Route>
|
||||
);
|
||||
</Route>
|
||||
);
|
||||
|
||||
initFormsy();
|
||||
initFormsy();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<Router history={browserHistory}>{routes}</Router>
|
||||
</I18nProvider>
|
||||
</Provider>,
|
||||
document.getElementById('react-app-index')
|
||||
);
|
||||
|
||||
});
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<Router history={browserHistory}>{routes}</Router>
|
||||
</I18nProvider>
|
||||
</Provider>,
|
||||
document.getElementById('react-app-index')
|
||||
);
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import when from 'when';
|
||||
|
||||
import TempStorageWorker from '../workers/TempStorageWorker';
|
||||
|
||||
class TempStorage {
|
||||
|
||||
constructor() {
|
||||
this._createWorkerInstance();
|
||||
// This promise is resolved when the store has been loaded from
|
||||
// the worker for the first time.
|
||||
this._def = when.defer();
|
||||
this.initialized = this._def.promise;
|
||||
this._initStore();
|
||||
}
|
||||
|
||||
_createWorkerInstance() {
|
||||
if(window && window.SharedWorker) {
|
||||
this.worker = new TempStorageWorker();
|
||||
this.worker.port.start();
|
||||
}
|
||||
}
|
||||
|
||||
_initStore() {
|
||||
if(this.worker) {
|
||||
|
||||
this.worker.port.onmessage = e => {
|
||||
if(e.data !== null && typeof e.data === 'object') {
|
||||
for(let key in e.data) {
|
||||
let val = e.data[key];
|
||||
if(val === undefined) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
else {
|
||||
// sessionStorage can only store text, so serialize if necessary.
|
||||
val = (typeof(val) === 'object') ? JSON.stringify(val) : val;
|
||||
sessionStorage.setItem(key, val);
|
||||
}
|
||||
}
|
||||
this._def.resolve(e.data);
|
||||
}
|
||||
};
|
||||
this.worker.port.postMessage(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from the store.
|
||||
*/
|
||||
getItem(key) {
|
||||
let item = sessionStorage.getItem(key);
|
||||
// Try to deserialize, if the original value was an object/array.
|
||||
try {
|
||||
return JSON.parse(item);
|
||||
} catch(err) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/modifiy an item in the store
|
||||
*/
|
||||
setItem(key, val) {
|
||||
let storeObj = {};
|
||||
// sessionStorage can only store text, so serialize if necessary.
|
||||
val = (typeof(val) === 'object') ? JSON.stringify(val) : val;
|
||||
storeObj[key] = val;
|
||||
this.worker.port.postMessage(storeObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the store
|
||||
*/
|
||||
removeItem(key) {
|
||||
let storeObj = {};
|
||||
storeObj[key] = undefined;
|
||||
this.worker.port.postMessage(storeObj);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new TempStorage();
|
|
@ -1,22 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let store = {};
|
||||
let ports = [];
|
||||
|
||||
self.onconnect = connEvent => {
|
||||
|
||||
ports.push(connEvent.ports[0]);
|
||||
connEvent.ports[0].onmessage = e => {
|
||||
if(e.data !== null) {
|
||||
if(typeof(e.data) === 'object') {
|
||||
for(let key in e.data) {
|
||||
store[key] = e.data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
ports.forEach(port => {
|
||||
port.postMessage(store);
|
||||
});
|
||||
};
|
||||
|
||||
};
|
|
@ -20,13 +20,6 @@ module.exports = {
|
|||
loader: 'babel'
|
||||
},
|
||||
|
||||
// Shared Workers
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: /src\/js\/workers/,
|
||||
loader: 'shared-worker!babel'
|
||||
},
|
||||
|
||||
// Images
|
||||
{
|
||||
test: /\.(png|jpg|gif)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
|
|
Loading…
Reference in New Issue