diff --git a/ghost/admin/app/services/ajax.js b/ghost/admin/app/services/ajax.js index 45d7d2923b..2cd1c4b9a0 100644 --- a/ghost/admin/app/services/ajax.js +++ b/ghost/admin/app/services/ajax.js @@ -133,12 +133,38 @@ let ajaxService = AjaxService.extend({ // ember-ajax recognises `application/vnd.api+json` as a JSON-API request // and formats appropriately, we want to handle `application/json` the same _makeRequest(hash) { + let isAuthenticated = this.get('session.isAuthenticated'); + let isGhostRequest = hash.url.indexOf('/ghost/api/') !== -1; + let isTokenRequest = isGhostRequest && hash.url.match(/authentication\/(?:token|ghost)/); + let tokenExpiry = this.get('session.authenticated.expires_at'); + let isTokenExpired = tokenExpiry < (new Date()).getTime(); + if (isJSONContentType(hash.contentType) && hash.type !== 'GET') { if (typeof hash.data === 'object') { hash.data = JSON.stringify(hash.data); } } + // we can get into a situation where the app is left open without a + // network connection and the token subsequently expires, this will + // result in the next network request returning a 401 and killing the + // session. This is an attempt to detect that and restore the session + // using the stored refresh token before continuing with the request + // + // TODO: + // - this might be quite blunt, if we have a lot of requests at once + // we probably want to queue the requests until the restore completes + // BUG: + // - the original caller gets a rejected promise with `undefined` instead + // of the AjaxError object when session restore fails. This isn't a + // huge deal because the session will be invalidated and app reloaded + // but it would be nice to be consistent + if (isAuthenticated && isGhostRequest && !isTokenRequest && isTokenExpired) { + return this.get('session').restore().then(() => { + return this._makeRequest(hash); + }); + } + return this._super(...arguments); }, diff --git a/ghost/admin/tests/integration/services/ajax-test.js b/ghost/admin/tests/integration/services/ajax-test.js index ad8d59a0e8..409c7b96ce 100644 --- a/ghost/admin/tests/integration/services/ajax-test.js +++ b/ghost/admin/tests/integration/services/ajax-test.js @@ -14,6 +14,8 @@ import { isUnsupportedMediaTypeError } from 'ghost-admin/services/ajax'; import config from 'ghost-admin/config/environment'; +import Service from 'ember-service'; +import RSVP from 'rsvp'; function stubAjaxEndpoint(server, response = {}, code = 200) { server.get('/test/', function () { @@ -176,5 +178,97 @@ describeModule( done(); }); }); + + /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ + describe('session handling', function () { + let successfulRequest = false; + + let sessionStub = Service.extend({ + isAuthenticated: true, + restoreCalled: false, + authenticated: null, + + init() { + this.authenticated = { + expires_at: (new Date()).getTime() - 10000, + refresh_token: 'RefreshMe123' + }; + }, + + restore() { + this.restoreCalled = true; + this.authenticated.expires_at = (new Date()).getTime() + 10000; + return RSVP.resolve(); + }, + + authorize() { + + } + }); + + beforeEach(function () { + server.get('/ghost/api/v0.1/test/', function () { + return [ + 200, + {'Content-Type': 'application/json'}, + JSON.stringify({ + success: true + }) + ]; + }); + + server.post('/ghost/api/v0.1/authentication/token', function () { + return [ + 401, + {'Content-Type': 'application/json'}, + JSON.stringify({}) + ]; + }); + }); + + it('can restore an expired session', function (done) { + let ajax = this.subject(); + ajax.set('session', sessionStub.create()); + + ajax.request('/ghost/api/v0.1/test/'); + + ajax.request('/ghost/api/v0.1/test/').then((result) => { + expect(ajax.get('session.restoreCalled'), 'restoreCalled').to.be.true; + expect(result.success, 'result.success').to.be.true; + done(); + }).catch((error) => { + expect(true, 'request failed').to.be.false; + done(); + }); + }); + + it('errors correctly when session restoration fails', function (done) { + let ajax = this.subject(); + let invalidateCalled = false; + + ajax.set('session', sessionStub.create()); + ajax.set('session.restore', function () { + this.set('restoreCalled', true); + return ajax.post('/ghost/api/v0.1/authentication/token'); + }); + ajax.set('session.invalidate', function () { + invalidateCalled = true; + }); + + stubAjaxEndpoint(server, {}, 401); + + ajax.request('/ghost/api/v0.1/test/').then(() => { + expect(true, 'request was successful').to.be.false; + done(); + }).catch((error) => { + // TODO: fix the error return when a session restore fails + // expect(isUnauthorizedError(error)).to.be.true; + expect(ajax.get('session.restoreCalled'), 'restoreCalled').to.be.true; + expect(successfulRequest, 'successfulRequest').to.be.false; + expect(invalidateCalled, 'invalidateCalled').to.be.true; + done(); + }); + }); + }); } );