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:
Honza Pokorny 2016-12-07 16:40:29 +01:00
parent c107619eda
commit 9c9061c53b
8 changed files with 93 additions and 265 deletions

View File

@ -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",

View File

@ -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();
});
});

View File

@ -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);
});
});
});

View File

@ -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());
};

View File

@ -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')
);

View File

@ -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();

View File

@ -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);
});
};
};

View File

@ -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+)?$/,