Commit 1e2450e9 authored by Robert Knight's avatar Robert Knight

Move session state to the Redux store

Move the session state out of the "session" service and into the Redux
store.

This enables logic in reducers and action creators to access the
logged-in user, list of groups and active feature flags.
parent f588f1e7
......@@ -39,6 +39,7 @@ var thunk = require('redux-thunk').default;
var reducers = require('./reducers');
var annotationsReducer = require('./reducers/annotations');
var selectionReducer = require('./reducers/selection');
var sessionReducer = require('./reducers/session');
var viewerReducer = require('./reducers/viewer');
var util = require('./reducers/util');
......@@ -90,6 +91,7 @@ module.exports = function ($rootScope, settings) {
var actionCreators = redux.bindActionCreators(Object.assign({},
annotationsReducer.actions,
selectionReducer.actions,
sessionReducer.actions,
viewerReducer.actions
), store.dispatch);
......
......@@ -12,7 +12,7 @@ function SidebarTutorialController(session) {
};
this.dismiss = function () {
session.dismiss_sidebar_tutorial();
session.dismissSidebarTutorial();
};
}
......
......@@ -19,6 +19,7 @@
var annotations = require('./annotations');
var selection = require('./selection');
var session = require('./session');
var viewer = require('./viewer');
var util = require('./util');
......@@ -27,6 +28,7 @@ function init(settings) {
{},
annotations.init(),
selection.init(settings),
session.init(),
viewer.init()
);
}
......@@ -34,6 +36,7 @@ function init(settings) {
var update = util.createReducer(Object.assign(
annotations.update,
selection.update,
session.update,
viewer.update
));
......
'use strict';
var util = require('./util');
function init() {
return {
/**
* The state of the user's login session.
*
* This includes their user ID, set of enabled features, and the list of
* groups they are a member of.
*/
session: {
/**
* The CSRF token for requests to API endpoints that use cookie
* authentication.
*/
csrf: null,
/** A map of features that are enabled for the current user. */
features: {},
/** List of groups that the current user is a member of. */
groups: [],
/**
* The authenticated user ID or null if the user is not logged in.
*/
userid: null,
},
};
}
var update = {
UPDATE_SESSION: function (state, action) {
return {
session: action.session,
};
},
};
var actions = util.actionTypes(update);
/**
* Update the session state.
*/
function updateSession(session) {
return {
type: actions.UPDATE_SESSION,
session: session,
};
}
module.exports = {
init: init,
update: update,
actions: {
updateSession: updateSession,
},
};
'use strict';
var session = require('../session');
var util = require('../util');
var init = session.init;
var actions = session.actions;
var update = util.createReducer(session.update);
describe('session reducer', function () {
describe('#updateSession', function () {
it('updates the session state', function () {
var newSession = Object.assign(init(), {userid: 'john'});
var state = update(init(), actions.updateSession(newSession));
assert.deepEqual(state.session, newSession);
});
});
});
......@@ -48,7 +48,7 @@ function sessionActions(options) {
*
* @ngInject
*/
function session($http, $resource, $rootScope, flash, raven, settings) {
function session($http, $resource, $rootScope, annotationUI, flash, raven, settings) {
// Headers sent by every request made by the session service.
var headers = {};
var actions = sessionActions({
......@@ -59,9 +59,6 @@ function session($http, $resource, $rootScope, flash, raven, settings) {
var endpoint = new URL('app/:path', settings.serviceUrl).href;
var resource = $resource(endpoint, {}, actions);
// Blank initial model state
resource.state = {};
// Cache the result of _load()
var lastLoad;
var lastLoadTime;
......@@ -108,20 +105,22 @@ function session($http, $resource, $rootScope, flash, raven, settings) {
* when new state has been pushed to it by the server.
*/
resource.update = function (model) {
var isInitialLoad = !resource.state.csrf;
var prevSession = annotationUI.getState().session;
var isInitialLoad = !prevSession.csrf;
var userChanged = model.userid !== resource.state.userid;
var groupsChanged = !angular.equals(model.groups, resource.state.groups);
var userChanged = model.userid !== prevSession.userid;
var groupsChanged = !angular.equals(model.groups, prevSession.groups);
// Copy the model data (including the CSRF token) into `resource.state`.
angular.copy(model, resource.state);
// Update the session model used by the application
annotationUI.updateSession(model);
// Set up subsequent requests to send the CSRF token in the headers.
if (resource.state.csrf) {
headers[$http.defaults.xsrfHeaderName] = resource.state.csrf;
if (model.csrf) {
headers[$http.defaults.xsrfHeaderName] = model.csrf;
}
lastLoad = Promise.resolve(resource.state);
lastLoad = Promise.resolve(model);
lastLoadTime = Date.now();
$rootScope.$broadcast(events.SESSION_CHANGED, {
......@@ -135,9 +134,9 @@ function session($http, $resource, $rootScope, flash, raven, settings) {
});
// associate error reports with the current user in Sentry
if (resource.state.userid) {
if (model.userid) {
raven.setUserInfo({
id: resource.state.userid,
id: model.userid,
});
} else {
raven.setUserInfo(undefined);
......@@ -183,7 +182,21 @@ function session($http, $resource, $rootScope, flash, raven, settings) {
return resource.update(model);
}
return resource;
return {
dismissSidebarTutorial: resource.dismiss_sidebar_tutorial,
load: resource.load,
login: resource.login,
logout: resource.logout,
// For the moment, we continue to expose the session state as a property on
// this service. In future, other services which access the session state
// will do so directly from annotationUI or via selector functions
get state() {
return annotationUI.getState().session;
},
update: resource.update,
};
}
module.exports = session;
......@@ -20,21 +20,32 @@ describe('h:session', function () {
.service('session', require('../session'));
});
beforeEach(mock.module('h'));
beforeEach(mock.module(function ($provide) {
beforeEach(function () {
sandbox = sinon.sandbox.create();
var state = {};
var fakeAnnotationUI = {
getState: function () {
return {session: state};
},
updateSession: function (session) {
state = session;
},
};
fakeFlash = {error: sandbox.spy()};
fakeRaven = {
setUserInfo: sandbox.spy(),
};
$provide.value('settings', {
mock.module('h', {
annotationUI: fakeAnnotationUI,
flash: fakeFlash,
raven: fakeRaven,
settings: {
serviceUrl: 'https://test.hypothes.is/root/',
},
});
});
$provide.value('flash', fakeFlash);
$provide.value('raven', fakeRaven);
}));
beforeEach(mock.inject(function (_$httpBackend_, _$rootScope_, _session_) {
......@@ -237,11 +248,11 @@ describe('h:session', function () {
});
});
describe('#dismiss_sidebar_tutorial()', function () {
describe('#dismissSidebarTutorial()', function () {
var url = 'https://test.hypothes.is/root/app/dismiss_sidebar_tutorial';
it('disables the tutorial for the user', function () {
$httpBackend.expectPOST(url).respond({});
session.dismiss_sidebar_tutorial();
session.dismissSidebarTutorial();
$httpBackend.flush();
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment