Skip to content

Commit

Permalink
Fix auth regressions after ESA 1.0 upgrade
Browse files Browse the repository at this point in the history
refs #6039, closes #6047, closes #6048

- delete old/unused fixtures file
- add failing tests for #6047 & #6048
- redirect to sign-in if we get a 401 when making an API request
- fix incorrect `this.notifications` call in tag controller
- raise `authorizationFailed` action in application route's `sessionInvalidated` hook so that it can be handled by leaf routes (fixes re-auth modal display)
- close "saving failed" alert when successfully re-authenticated
- adds a "window-proxy" util so that we can override `window.*` operations in tests
- fix `gh-selectize` attempting to register event handlers when the component has already been destroyed
  • Loading branch information
kevinansfield committed Nov 12, 2015
1 parent 2cfc46d commit 73ea9f5
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 71 deletions.
15 changes: 15 additions & 0 deletions core/client/app/adapters/base.js
@@ -1,10 +1,15 @@
import DS from 'ember-data';
import ghostPaths from 'ghost/utils/ghost-paths';
import Ember from 'ember';

const {inject} = Ember;

export default DS.RESTAdapter.extend({
host: window.location.origin,
namespace: ghostPaths().apiRoot.slice(1),

session: inject.service('session'),

shouldBackgroundReloadRecord: function () {
return false;
},
Expand Down Expand Up @@ -43,5 +48,15 @@ export default DS.RESTAdapter.extend({
return response.then(function () {
return null;
});
},

handleResponse: function (status) {
if (status === 401) {
if (this.get('session.isAuthenticated')) {
this.get('session').invalidate();
}
}

return this._super(...arguments);
}
});
22 changes: 12 additions & 10 deletions core/client/app/components/gh-selectize.js
Expand Up @@ -17,16 +17,18 @@ export default EmberSelectizeComponent.extend({
if (!openOnFocus) {
Ember.run.next(this, function () {
var selectize = this._selectize;
selectize.on('dropdown_open', function () {
if (Ember.isBlank(selectize.$control_input.val())) {
selectize.close();
}
});
selectize.on('type', function (filter) {
if (Ember.isBlank(filter)) {
selectize.close();
}
});
if (selectize) {
selectize.on('dropdown_open', function () {
if (Ember.isBlank(selectize.$control_input.val())) {
selectize.close();
}
});
selectize.on('type', function (filter) {
if (Ember.isBlank(filter)) {
selectize.close();
}
});
}
});
}
}),
Expand Down
1 change: 1 addition & 0 deletions core/client/app/controllers/modals/signin.js
Expand Up @@ -24,6 +24,7 @@ export default Ember.Controller.extend(ValidationEngine, {
this.get('session').authenticate(authStrategy, this.get('identification'), this.get('password')).then(function () {
self.send('closeModal');
self.set('password', '');
self.get('notifications').closeAlerts('post.save');
}).catch(function () {
// if authentication fails a rejected promise will be returned.
// it needs to be caught so it doesn't generate an exception in the console,
Expand Down
5 changes: 3 additions & 2 deletions core/client/app/controllers/settings/tags/tag.js
Expand Up @@ -9,6 +9,7 @@ export default Ember.Controller.extend({
isMobile: alias('tagsController.isMobile'),

tagsController: inject.controller('settings.tags'),
notifications: inject.service(),

saveTagProperty: function (propKey, newValue) {
const tag = this.get('tag'),
Expand All @@ -27,10 +28,10 @@ export default Ember.Controller.extend({

tag.save().then((savedTag) => {
// replace 'new' route with 'tag' route
this.replaceWith('settings.tags.tag', savedTag);
this.replaceRoute('settings.tags.tag', savedTag);
}).catch((error) => {
if (error) {
this.notifications.showAPIError(error, {key: 'tag.save'});
this.get('notifications').showAPIError(error, {key: 'tag.save'});
}
});
},
Expand Down
31 changes: 30 additions & 1 deletion core/client/app/mirage/config.js
Expand Up @@ -52,6 +52,23 @@ export default function () {

this.get('/notifications/', 'notifications');

/* Posts ---------------------------------------------------------------- */

this.post('/posts/', function (db, request) {
const [attrs] = JSON.parse(request.requestBody).posts;
let post;

if (isBlank(attrs.slug) && !isBlank(attrs.title)) {
attrs.slug = attrs.title.dasherize();
}

post = db.posts.insert(attrs);

return {
posts: [post]
};
});

/* Settings ------------------------------------------------------------- */

this.get('/settings/', function (db, request) {
Expand Down Expand Up @@ -84,6 +101,16 @@ export default function () {
};
});

/* Slugs ---------------------------------------------------------------- */

this.get('/slugs/post/:slug/', function (db, request) {
return {
slugs: [
{slug: request.params.slug.dasherize}
]
};
});

/* Tags ----------------------------------------------------------------- */

this.post('/tags/', function (db, request) {
Expand Down Expand Up @@ -119,7 +146,7 @@ export default function () {

this.put('/tags/:id/', function (db, request) {
const id = request.params.id,
[attrs] = JSON.parse(request.requestBody).contacts,
[attrs] = JSON.parse(request.requestBody).tags,
record = db.tags.update(id, attrs);

return {
Expand All @@ -137,6 +164,8 @@ export default function () {
users: [db.users.find(1)]
};
});

this.get('/users/', 'users');
}

/*
Expand Down
6 changes: 6 additions & 0 deletions core/client/app/mirage/factories/post.js
@@ -0,0 +1,6 @@
/* jscs:disable */
import Mirage from 'ember-cli-mirage';

export default Mirage.Factory.extend({
// TODO: fill in with actual factory data
});
10 changes: 10 additions & 0 deletions core/client/app/routes/application.js
@@ -1,9 +1,11 @@
/* global key */

import Ember from 'ember';
import AuthConfiguration from 'ember-simple-auth/configuration';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
import windowProxy from 'ghost/utils/window-proxy';

const shortcuts = {};

Expand Down Expand Up @@ -42,6 +44,10 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
});
},

sessionInvalidated: function () {
this.send('authorizationFailed');
},

actions: {
openMobileMenu: function () {
this.controller.set('showMobileMenu', true);
Expand Down Expand Up @@ -75,6 +81,10 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
});
},

authorizationFailed: function () {
windowProxy.replaceLocation(AuthConfiguration.baseURL);
},

openModal: function (modalName, model, type) {
this.get('dropdown').closeDropdowns();
key.setScope('modal');
Expand Down
4 changes: 2 additions & 2 deletions core/client/app/routes/editor/edit.js
Expand Up @@ -36,7 +36,7 @@ export default AuthenticatedRoute.extend(base, {
return post;
}

return self.replaceWith('posts.index');
return self.replaceRoute('posts.index');
});
},

Expand All @@ -45,7 +45,7 @@ export default AuthenticatedRoute.extend(base, {

return self.get('session.user').then(function (user) {
if (user.get('isAuthor') && !post.isAuthoredByUser(user)) {
return self.replaceWith('posts.index');
return self.replaceRoute('posts.index');
}
});
},
Expand Down
4 changes: 2 additions & 2 deletions core/client/app/routes/posts/post.js
Expand Up @@ -32,7 +32,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
return post;
}

return self.replaceWith('posts.index');
return self.replaceRoute('posts.index');
});
},

Expand All @@ -41,7 +41,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {

return self.get('session.user').then(function (user) {
if (user.get('isAuthor') && !post.isAuthoredByUser(user)) {
return self.replaceWith('posts.index');
return self.replaceRoute('posts.index');
}
});
},
Expand Down
3 changes: 1 addition & 2 deletions core/client/app/templates/editor/edit.hbs
@@ -1,8 +1,7 @@
{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor|}}
<header class="view-header">
{{#gh-view-title classNames="gh-editor-title" openMobileMenu="openMobileMenu"}}
{{gh-trim-focus-input type="text" id="entry-title"placeholder="Your Post Title" value=model.titleScratch
tabindex="1" focus=shouldFocusTitle}}
{{gh-trim-focus-input type="text" id="entry-title" placeholder="Your Post Title" value=model.titleScratch tabindex="1" focus=shouldFocusTitle}}
{{/gh-view-title}}
<section class="view-actions">
<button type="button" class="post-settings" title="Post Settings" {{action "openSettingsMenu"}}>
Expand Down
9 changes: 9 additions & 0 deletions core/client/app/utils/window-proxy.js
@@ -0,0 +1,9 @@
export default {
changeLocation: function (url) {
window.location = url;
},

replaceLocation: function (url) {
window.location.replace(url);
}
};
132 changes: 132 additions & 0 deletions core/client/tests/acceptance/authentication-test.js
@@ -0,0 +1,132 @@
/* jshint expr:true */
import {
describe,
it,
beforeEach,
afterEach
} from 'mocha';
import { expect } from 'chai';
import Ember from 'ember';
import startApp from '../helpers/start-app';
import { authenticateSession, currentSession, invalidateSession } from 'ghost/tests/helpers/ember-simple-auth';
import Mirage from 'ember-cli-mirage';
import windowProxy from 'ghost/utils/window-proxy';

const {run} = Ember;

describe('Acceptance: Authentication', function () {
let application,
originalReplaceLocation;

beforeEach(function () {
application = startApp();
});

afterEach(function () {
run(application, 'destroy');
});

describe('general page', function () {
beforeEach(function () {
originalReplaceLocation = windowProxy.replaceLocation;
windowProxy.replaceLocation = function (url) {
visit(url);
};

server.loadFixtures();
const role = server.create('role', {name: 'Administrator'}),
user = server.create('user', {roles: [role], slug: 'test-user'});
});

afterEach(function () {
windowProxy.replaceLocation = originalReplaceLocation;
});

it('invalidates session on 401 API response', function () {
const role = server.create('role', {name: 'Administrator'}),
user = server.create('user', {roles: [role]});

// return a 401 when attempting to retrieve tags
server.get('/users/', (db, request) => {
return new Mirage.Response(401, {}, {
errors: [
{message: 'Access denied.', errorType: 'UnauthorizedError'}
]
});
});

authenticateSession(application);
visit('/team');

andThen(() => {
expect(currentURL(), 'url after 401').to.equal('/signin');
});
});
});

describe('editor', function () {
let origDebounce = Ember.run.debounce;
let origThrottle = Ember.run.throttle;

// we don't want the autosave interfering in this test
beforeEach(function () {
Ember.run.debounce = function () { };
Ember.run.throttle = function () { };
});

it('displays re-auth modal attempting to save with invalid session', function () {
const role = server.create('role', {name: 'Administrator'}),
user = server.create('user', {roles: [role]});

// simulate an invalid session when saving the edited post
server.put('/posts/:id/', (db, request) => {
let post = db.posts.find(request.params.id),
[attrs] = JSON.parse(request.requestBody).posts;

if (attrs.markdown === 'Edited post body') {
return new Mirage.Response(401, {}, {
errors: [
{message: 'Access denied.', errorType: 'UnauthorizedError'}
]
});
} else {
return {
posts: [post]
};
}
});

server.loadFixtures();
authenticateSession(application);

visit('/editor');

// create the post
fillIn('#entry-title', 'Test Post');
fillIn('textarea.markdown-editor', 'Test post body');
click('.js-publish-button');

andThen(() => {
// we shouldn't have a modal at this point
expect(find('.modal-container #login').length, 'modal exists').to.equal(0);
// we also shouldn't have any alerts
expect(find('.gh-alert').length, 'no of alerts').to.equal(0);
});

// update the post
fillIn('textarea.markdown-editor', 'Edited post body');
click('.js-publish-button');

andThen(() => {
// we should see a re-auth modal
expect(find('.modal-container #login').length, 'modal exists').to.equal(1);
});
});

// don't clobber debounce/throttle for future tests
afterEach(function () {
Ember.run.debounce = origDebounce;
Ember.run.throttle = origThrottle;
});
});
});

0 comments on commit 73ea9f5

Please sign in to comment.