Commit ed44cb77 authored by Nick Stenning's avatar Nick Stenning

Consolidate session service and session helpers

This commit does three things. In order of importance:

- wraps the ngResource for managing session state in a service, which
  allows us to expose current session state as `session.state` on that
  instance. This means that components of the application that don't
  want to alter or explicitly fetch session state, but wish to display
  some part of it (such as the current authenticated userid, etc.) can
  do so more easily.
- moves configuration of the session resources into the session service
  rather than requiring configuration from outside via a
  sessionProvider.
- translates the session service to JavaScript from CoffeeScript.
parent b29c9bac
......@@ -121,7 +121,6 @@ module.exports = angular.module('h', [
.filter('urlencode', require('./filter/urlencode'))
.provider('identity', require('./identity'))
.provider('session', require('./session'))
.service('annotator', -> new Annotator(angular.element('<div>')))
.service('annotationMapper', require('./annotation-mapper'))
......@@ -140,6 +139,7 @@ module.exports = angular.module('h', [
.service('queryParser', require('./query-parser'))
.service('render', require('./render'))
.service('searchFilter', require('./search-filter'))
.service('session', require('./session'))
.service('store', require('./store'))
.service('streamFilter', require('./stream-filter'))
.service('streamer', require('./streamer'))
......
var angular = require('angular');
var SESSION_ACTIONS = [
'login', 'logout', 'register', 'forgot_password',
'reset_password', 'edit_profile', 'disable_user'
];
configure.$inject = ['$httpProvider', 'identityProvider', 'sessionProvider'];
function configure( $httpProvider, identityProvider, sessionProvider) {
configure.$inject = ['$httpProvider', 'identityProvider'];
function configure( $httpProvider, identityProvider) {
// Pending authentication check
var authCheck = null;
......@@ -66,30 +60,6 @@ function configure( $httpProvider, identityProvider, sessionProvider) {
});
}
];
sessionProvider.actions.load = {
method: 'GET',
withCredentials: true
};
sessionProvider.actions.profile = {
method: 'GET',
params: {
__formid__: 'profile'
},
withCredentials: true
};
for (var i = 0; i < SESSION_ACTIONS.length; i++) {
var action = SESSION_ACTIONS[i];
sessionProvider.actions[action] = {
method: 'POST',
params: {
__formid__: action
},
withCredentials: true
};
}
}
angular.module('h')
......
angular = require('angular')
###*
# @ngdoc provider
# @name sessionProvider
# @property {Object} actions additional actions to mix into the resource.
# @property {Object} options additional options mix into resource actions.
# @description
# This class provides an angular $resource factory as an angular service
# for manipulating server-side sessions. It defines the REST-ish actions
# that return the state of the users session after modifying it through
# registration, authentication, or account management.
###
module.exports = class SessionProvider
actions: null
options: null
constructor: ->
@actions = {}
@options = {}
###*
# @ngdoc service
# @name session
# @description
# An angular resource factory for sessions. See the documentation for
# {@link sessionProvider sessionProvider} for ways to configure the
# resource.
#
# @example
# Using the session with BrowserID.
#
# navigator.id.beginAuthentication(function (email) {
# session.load(function (data) {
# var user = data.user;
# if(user && user.email == email) {
# navigator.id.completeAuthentication();
# } else {
# displayLoginForm();
# }
# });
# });
###
$get: [
'$document', '$http', '$q', '$resource', 'flash', 'xsrf',
($document, $http, $q, $resource, flash, xsrf) ->
actions = {}
provider = this
prepare = (data, headersGetter) ->
headersGetter()[$http.defaults.xsrfHeaderName] = xsrf.token
return angular.toJson data
process = (data, headersGetter) ->
# Parse as json
data = angular.fromJson data
# Lift response data
model = data.model or {}
model.errors = data.errors
model.reason = data.reason
# Fire flash messages.
for q, msgs of data.flash
for m in msgs
flash[q](m)
xsrf.token = model.csrf
# Return the model
model
for name, options of provider.actions
actions[name] = angular.extend {}, options, @options
actions[name].transformRequest = prepare
actions[name].transformResponse = process
base = $document.prop('baseURI')
endpoint = new URL('/app', base).href
$resource(endpoint, {}, actions)
]
'use strict';
var angular = require('angular');
var ACCOUNT_ACTIONS = [
['login', 'POST'],
['logout', 'POST'],
['register', 'POST'],
['forgot_password', 'POST'],
['reset_password', 'POST'],
['profile', 'GET'],
['edit_profile', 'POST'],
['disable_user', 'POST']
];
function sessionActions(options) {
var actions = {};
// These map directly to views in `h.accounts`, and all have a similar form:
for (var i = 0, len = ACCOUNT_ACTIONS.length; i < len; i++) {
var name = ACCOUNT_ACTIONS[i][0];
var method = ACCOUNT_ACTIONS[i][1];
actions[name] = {
method: method,
params: {
__formid__: name
}
};
}
// Finally, add a simple method for getting the current session state
actions.load = {method: 'GET'};
if (typeof options !== 'undefined') {
for (var act in actions) {
for (var opt in options) {
actions[act][opt] = options[opt];
}
}
}
return actions;
}
/**
* @ngdoc service
* @name session
* @description
* Access to the application session and account actions. This service gives
* other parts of the application access to parts of the server-side session
* state (such as current authenticated userid, CSRF token, etc.).
*
* In addition, this service also provides helper methods for mutating the
* session state, by, e.g. logging in, logging out, etc.
*/
// TODO: Move accounts data management (e.g. profile, edit_profile,
// disable_user, etc) into another module with another route.
session.$inject = ['$document', '$http', '$resource', 'flash', 'xsrf'];
function session( $document, $http, $resource, flash, xsrf) {
var actions = sessionActions({
transformRequest: prepare,
transformResponse: process,
withCredentials: true
});
var base = $document.prop('baseURI');
var endpoint = new URL('/app', base).href;
var resource = $resource(endpoint, {}, actions);
// Blank inital model state
resource.state = {};
function prepare(data, headersGetter) {
headersGetter()[$http.defaults.xsrfHeaderName] = xsrf.token;
return angular.toJson(data);
}
function process(data, headersGetter) {
// Parse as json
data = angular.fromJson(data);
// Lift response data
var model = data.model || {};
if (typeof data.errors !== 'undefined') {
model.errors = data.errors;
}
if (typeof data.reason !== 'undefined') {
model.reason = data.reason;
}
// Fire flash messages.
for (var type in data.flash) {
if (data.flash.hasOwnProperty(type)) {
var msgs = data.flash[type];
for (var i = 0, len = msgs.length; i < len; i++) {
flash[type](msgs[i]);
}
}
}
xsrf.token = model.csrf;
angular.copy(model, resource.state);
// Return the model
return model;
}
return resource;
}
module.exports = session;
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'session', ->
fakeFlash = null
fakeDocument = null
fakeXsrf = null
sandbox = null
before ->
angular.module('h', ['ngResource'])
.provider('session', require('../session'))
beforeEach module('h')
beforeEach module ($provide, sessionProvider) ->
sandbox = sinon.sandbox.create()
fakeDocument = {prop: -> '/session'}
fakeFlash = error: sandbox.spy()
fakeXsrf = {token: 'faketoken'}
$provide.value '$document', fakeDocument
$provide.value 'flash', fakeFlash
$provide.value 'xsrf', fakeXsrf
sessionProvider.actions =
login:
url: '/login'
method: 'POST'
return
afterEach ->
sandbox.restore()
describe 'sessionService', ->
$httpBackend = null
session = null
beforeEach inject (_$httpBackend_, _session_) ->
$httpBackend = _$httpBackend_
session = _session_
describe '#<action>()', ->
url = '/login'
it 'should send an HTTP POST to the action', ->
$httpBackend.expectPOST(url, code: 123).respond({})
result = session.login(code: 123)
$httpBackend.flush()
it 'should invoke the flash service with any flash messages', ->
response =
flash:
error: ['fail']
$httpBackend.expectPOST(url).respond(response)
result = session.login({})
$httpBackend.flush()
assert.calledWith fakeFlash.error, 'fail'
it 'should assign errors and status reasons to the model', ->
response =
model:
userid: 'alice'
errors:
password: 'missing'
reason: 'bad credentials'
$httpBackend.expectPOST(url).respond(response)
result = session.login({})
$httpBackend.flush()
assert.match result, response.model, 'the model is present'
assert.match result.errors, response.errors, 'the errors are present'
assert.match result.reason, response.reason, 'the reason is present'
it 'should capture and send the xsrf token', ->
token = 'deadbeef'
headers =
'Accept': 'application/json, text/plain, */*'
'Content-Type': 'application/json;charset=utf-8'
'X-XSRF-TOKEN': token
model = {csrf: token}
request = $httpBackend.expectPOST(url).respond({model})
result = session.login({})
$httpBackend.flush()
assert.equal fakeXsrf.token, token
$httpBackend.expectPOST(url, {}, headers).respond({})
session.login({})
$httpBackend.flush()
"use strict";
var mock = require('angular-mock');
var assert = chai.assert;
sinon.assert.expose(assert, {prefix: null});
describe('h:session', function () {
var $httpBackend;
var fakeFlash;
var fakeXsrf;
var sandbox;
var session;
before(function () {
angular.module('h', ['ngResource'])
.service('session', require('../session'));
});
beforeEach(mock.module('h'));
beforeEach(mock.module(function ($provide) {
sandbox = sinon.sandbox.create();
var fakeDocument = {
prop: sandbox.stub()
};
fakeDocument.prop.withArgs('baseURI').returns('http://foo.com/');
fakeFlash = {error: sandbox.spy()};
fakeXsrf = {token: 'faketoken'};
$provide.value('$document', fakeDocument);
$provide.value('flash', fakeFlash);
$provide.value('xsrf', fakeXsrf);
}));
beforeEach(mock.inject(function (_$httpBackend_, _session_) {
$httpBackend = _$httpBackend_;
session = _session_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
sandbox.restore();
});
// There's little point testing every single route here, as they're
// declarative and ultimately we'd be testing ngResource.
describe('#login()', function () {
var url = 'http://foo.com/app?__formid__=login';
it('should send an HTTP POST to the action', function () {
$httpBackend.expectPOST(url, {code: 123}).respond({});
session.login({code: 123});
$httpBackend.flush();
});
it('should invoke the flash service with any flash messages', function () {
var response = {
flash: {
error: ['fail']
}
};
$httpBackend.expectPOST(url).respond(response);
session.login({});
$httpBackend.flush();
assert.calledWith(fakeFlash.error, 'fail');
});
it('should assign errors and status reasons to the model', function () {
var response = {
model: {
userid: 'alice'
},
errors: {
password: 'missing'
},
reason: 'bad credentials'
};
$httpBackend.expectPOST(url).respond(response);
var result = session.login({});
$httpBackend.flush();
assert.match(result, response.model, 'the model is present');
assert.match(result.errors, response.errors, 'the errors are present');
assert.match(result.reason, response.reason, 'the reason is present');
});
it('should capture and send the xsrf token', function () {
var token = 'deadbeef';
var headers = {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json;charset=utf-8',
'X-XSRF-TOKEN': token
};
var model = {csrf: token};
$httpBackend.expectPOST(url).respond({model: model});
session.login({});
$httpBackend.flush();
assert.equal(fakeXsrf.token, token);
$httpBackend.expectPOST(url, {}, headers).respond({});
session.login({});
$httpBackend.flush();
});
it('should expose the model as session.state', function () {
var response = {
model: {
userid: 'alice'
}
};
assert.deepEqual(session.state, {});
$httpBackend.expectPOST(url).respond(response);
session.login({});
$httpBackend.flush();
assert.deepEqual(session.state, response.model);
});
});
});
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