Commit f4e9e5de authored by Randall Leeds's avatar Randall Leeds

Merge pull request #2403 from hypothesis/simpler-session-service

Simpler session service
parents b29c9bac 3ed424cb
......@@ -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,32 +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')
.value('xsrf', {token: null})
.config(configure);
angular.module('h').config(configure);
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'];
function session( $document, $http, $resource, flash) {
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) {
var csrfTok = resource.state.csrf;
if (typeof csrfTok !== 'undefined') {
headersGetter()[$http.defaults.xsrfHeaderName] = csrfTok;
}
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]);
}
}
}
// Copy the model data (including the CSRF token) into `resource.state`.
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()};
$provide.value('$document', fakeDocument);
$provide.value('flash', fakeFlash);
}));
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(session.state.csrf, 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