Made session.user a synchronous property rather than a promise

no issue

Having `session.user` return a promise made dealing with it in components difficult because you always had to remember it returned a promise rather than a model and had to handle the async behaviour. It also meant that you couldn't use any current user properties directly inside getters which made refactors to Glimmer/Octane idioms harder to reason about.

`session.user` was a cached computed property so it really made no sense for it to be a promise - it was loaded on first access and then always returned instantly but with a fulfilled promise rather than the  underlying model.

Refactoring to a synchronous property that is loaded as part of the authentication flows (we load the current user to check that we're logged in - we may as well make use of that!) means one less thing to be aware of/remember and provides a nicer migration process to Glimmer components. As part of the refactor, the auth flows and pre-load of required data across other services was also simplified to make it easier to find and follow.

- refactored app setup and `session.user`
  - added `session.populateUser()` that fetches a user model from the current user endpoint and sets it on `session.user`
  - removed knowledge of app setup from the `cookie` authenticator and moved it into = `session.postAuthPreparation()`, this means we have the same post-authentication setup no matter which authenticator is used so we have more consistent behaviour in tests which don't use the `cookie` authenticator
  - switched `session` service to native class syntax to get the expected `super()` behaviour
  - updated `handleAuthentication()` so it populate's `session.user` and performs post-auth setup before transitioning (handles sign-in after app load)
  - updated `application` route to remove duplicated knowledge of app preload behaviour that now lives in `session.postAuthPreparation()` (handles already-authed app load)
  - removed out-of-date attempt at pre-loading data from setup controller as that's now handled automatically via `session.handleAuthentication`
- updated app code to not treat `session.user` as a promise
  - predominant usage was router `beforeModel` hooks that transitioned users without valid permissions, this sets us up for an easier removal of the `current-user-settings` mixin in the future
This commit is contained in:
Kevin Ansfield 2021-07-08 14:37:31 +01:00
parent cc25b2348a
commit c646e78fff
43 changed files with 311 additions and 352 deletions

View File

@ -5,11 +5,7 @@ import {inject as service} from '@ember/service';
export default Authenticator.extend({
ajax: service(),
config: service(),
feature: service(),
ghostPaths: service(),
settings: service(),
whatsNew: service(),
sessionEndpoint: computed('ghostPaths.apiRoot', function () {
return `${this.ghostPaths.apiRoot}/session`;
@ -28,23 +24,7 @@ export default Authenticator.extend({
dataType: 'text'
};
return this.ajax.post(this.sessionEndpoint, options).then((authResult) => {
// TODO: remove duplication with application.afterModel
let preloadPromises = [
this.config.fetchAuthenticated(),
this.feature.fetch(),
this.settings.fetch()
];
// kick off background update of "whats new"
// - we don't want to block the router for this
// - we need the user details to know what the user has seen
this.whatsNew.fetchLatest.perform();
return RSVP.all(preloadPromises).then(() => {
return authResult;
});
});
return this.ajax.post(this.sessionEndpoint, options);
},
invalidate() {

View File

@ -12,7 +12,7 @@
<div class="gh-post-preview-email-footer">
<span class="mr3 nowrap fw6 f8 darkgrey">Send a test newsletter</span>
<div class="gh-post-preview-email-input {{if this.sendPreviewEmailError "error"}}" {{did-insert this.initPreviewEmailAddress}}>
<div class="gh-post-preview-email-input {{if this.sendPreviewEmailError "error"}}">
<Input
@value={{this.previewEmailAddress}}
class="gh-input gh-post-preview-email-input"

View File

@ -28,7 +28,7 @@ export default class ModalPostPreviewEmailComponent extends Component {
@tracked html = '';
@tracked subject = '';
@tracked previewEmailAddress = '';
@tracked previewEmailAddress = this.session.user.email;
@tracked sendPreviewEmailError = '';
get mailgunIsEnabled() {
@ -49,12 +49,6 @@ export default class ModalPostPreviewEmailComponent extends Component {
}
}
@action
async initPreviewEmailAddress() {
const user = await this.session.user;
this.previewEmailAddress = user.email;
}
@task({drop: true})
*sendPreviewEmailTask() {
try {

View File

@ -22,7 +22,7 @@ export default Controller.extend({
ui: service(),
showBilling: computed.reads('config.hostSettings.billing.enabled'),
showNavMenu: computed('router.currentRouteName', 'session.{isAuthenticated,user.isFulfilled}', 'ui.isFullScreen', function () {
showNavMenu: computed('router.currentRouteName', 'session.{isAuthenticated,user}', 'ui.isFullScreen', function () {
let {router, session, ui} = this;
// if we're in fullscreen mode don't show the nav menu
@ -31,8 +31,8 @@ export default Controller.extend({
}
// we need to defer showing the navigation menu until the session.user
// promise has fulfilled so that gh-user-can-admin has the correct data
if (!session.isAuthenticated || !session.user.isFulfilled) {
// is populated so that gh-user-can-admin has the correct data
if (!session.isAuthenticated || !session.user) {
return false;
}

View File

@ -115,7 +115,7 @@ export default Controller.extend({
// https://github.com/emberjs/data/issues/4963
run.schedule('destroy', this, () => {
// Reload currentUser and set session
this.set('session.user', store.findRecord('user', currentUserId));
this.session.populateUser({id: currentUserId});
// TODO: keep as notification, add link to view content
notifications.showNotification('Import successful', {key: 'import.upload.success'});

View File

@ -1,6 +1,5 @@
/* eslint-disable camelcase, ghost/ember/alias-model-in-controller */
import Controller, {inject as controller} from '@ember/controller';
import RSVP from 'rsvp';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {get} from '@ember/object';
import {isInvalidError} from 'ember-ajax/errors';
@ -15,7 +14,6 @@ export default Controller.extend(ValidationEngine, {
ghostPaths: service(),
notifications: service(),
session: service(),
settings: service(),
// ValidationEngine settings
validationType: 'setup',
@ -51,7 +49,7 @@ export default Controller.extend(ValidationEngine, {
authenticate: task(function* (authStrategy, authentication) {
// we don't want to redirect after sign-in during setup
this.set('session.skipAuthSuccessHandler', true);
this.session.skipAuthSuccessHandler = true;
try {
let authResult = yield this.session
@ -141,15 +139,13 @@ export default Controller.extend(ValidationEngine, {
}
// Don't call the success handler, otherwise we will be redirected to admin
this.set('session.skipAuthSuccessHandler', true);
this.session.skipAuthSuccessHandler = true;
return this.session.authenticate('authenticator:cookie', data.email, data.password).then(() => {
this.set('blogCreated', true);
return this._afterAuthentication(result);
}).catch((error) => {
this._handleAuthenticationError(error);
}).finally(() => {
this.set('session.skipAuthSuccessHandler', undefined);
});
}).catch((error) => {
this._handleSaveError(error);
@ -179,20 +175,14 @@ export default Controller.extend(ValidationEngine, {
},
_afterAuthentication(result) {
// fetch settings and private config for synchronous access before transitioning
let fetchSettingsAndConfig = RSVP.all([
this.settings.fetch()
]);
if (this.profileImage) {
return this._sendImage(result.users[0])
.then(() => (fetchSettingsAndConfig))
.then(() => (this.transitionToRoute('setup.three')))
.catch((resp) => {
this.notifications.showAPIError(resp, {key: 'setup.blog-details'});
});
} else {
return fetchSettingsAndConfig.then(() => this.transitionToRoute('setup.three'));
return this.transitionToRoute('setup.three');
}
}
});

View File

@ -105,7 +105,7 @@ export default Controller.extend({
formData.append('file', imageFile, imageFile.name);
formData.append('purpose', 'profile_image');
let user = yield this.get('session.user');
let user = this.session.user;
let response = yield this.ajax.post(uploadUrl, {
data: formData,
processData: false,

View File

@ -1,23 +1,15 @@
import Mixin from '@ember/object/mixin';
export default Mixin.create({
transitionAuthor() {
return (user) => {
if (user.get('isAuthorOrContributor')) {
return this.transitionTo('staff.user', user);
}
return user;
};
transitionAuthor(user) {
if (user.isAuthorOrContributor) {
return this.transitionTo('staff.user', user);
}
},
transitionEditor() {
return (user) => {
if (user.get('isEditor')) {
return this.transitionTo('staff');
}
return user;
};
transitionEditor(user) {
if (user.isEditor) {
return this.transitionTo('staff');
}
}
});

View File

@ -1,11 +1,9 @@
import AuthConfiguration from 'ember-simple-auth/configuration';
import RSVP from 'rsvp';
import Route from '@ember/routing/route';
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {InitSentryForEmber} from '@sentry/ember';
import {configureScope} from '@sentry/browser';
import {
isAjaxError,
isNotFoundError,
@ -51,57 +49,14 @@ export default Route.extend(ShortcutsRoute, {
},
beforeModel() {
return this.config.fetchUnauthenticated()
.then(() => {
// init Sentry here rather than app.js so that we can use API-supplied
// sentry_dsn and sentry_env rather than building it into release assets
if (this.config.get('sentry_dsn')) {
InitSentryForEmber({
dsn: this.config.get('sentry_dsn'),
environment: this.config.get('sentry_env'),
release: `ghost@${this.config.get('version')}`
});
}
});
return this.prepareApp();
},
afterModel(model, transition) {
async afterModel(model, transition) {
this._super(...arguments);
if (this.get('session.isAuthenticated')) {
this.session.appLoadTransition = transition;
this.session.loadServerNotifications();
// return the feature/settings load promises so that we block until
// they are loaded to enable synchronous access everywhere
return RSVP.all([
this.config.fetchAuthenticated(),
this.feature.fetch(),
this.settings.fetch()
]).then((results) => {
this._appLoaded = true;
// update Sentry with the full Ghost version which we only get after authentication
if (this.config.get('sentry_dsn')) {
configureScope((scope) => {
scope.addEventProcessor((event) => {
return new Promise((resolve) => {
resolve({
...event,
release: `ghost@${this.config.get('version')}`
});
});
});
});
}
// kick off background update of "whats new"
// - we don't want to block the router for this
// - we need the user details to know what the user has seen
this.whatsNew.fetchLatest.perform();
return results;
});
}
this._appLoaded = true;
@ -186,5 +141,30 @@ export default Route.extend(ShortcutsRoute, {
// fallback to 500 error page
return true;
}
},
async prepareApp() {
await this.config.fetchUnauthenticated();
// init Sentry here rather than app.js so that we can use API-supplied
// sentry_dsn and sentry_env rather than building it into release assets
if (this.config.get('sentry_dsn')) {
InitSentryForEmber({
dsn: this.config.get('sentry_dsn'),
environment: this.config.get('sentry_env'),
release: `ghost@${this.config.get('version')}`
});
}
if (this.session.isAuthenticated) {
try {
await this.session.populateUser();
} catch (e) {
await this.session.invalidate();
}
await this.session.postAuthPreparation();
}
}
});

View File

@ -3,11 +3,9 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default class DashboardRoute extends AuthenticatedRoute {
beforeModel() {
super.beforeModel(...arguments);
return this.session.user.then((user) => {
if (!user.isOwnerOrAdmin) {
return this.transitionTo('site');
}
});
if (!this.session.user.isOwnerOrAdmin) {
return this.transitionTo('site');
}
}
buildRouteInfoMetadata() {

View File

@ -37,18 +37,17 @@ export default AuthenticatedRoute.extend({
afterModel(post) {
this._super(...arguments);
return this.get('session.user').then((user) => {
let returnRoute = pluralize(post.constructor.modelName);
const user = this.session.user;
const returnRoute = pluralize(post.constructor.modelName);
if (user.get('isAuthorOrContributor') && !post.isAuthoredByUser(user)) {
return this.replaceWith(returnRoute);
}
if (user.isAuthorOrContributor && !post.isAuthoredByUser(user)) {
return this.replaceWith(returnRoute);
}
// If the post is not a draft and user is contributor, redirect to index
if (user.get('isContributor') && !post.get('isDraft')) {
return this.replaceWith(returnRoute);
}
});
// If the post is not a draft and user is contributor, redirect to index
if (user.isContributor && !post.isDraft) {
return this.replaceWith(returnRoute);
}
},
serialize(model) {

View File

@ -9,9 +9,7 @@ export default AuthenticatedRoute.extend({
return this.replaceWith('error404', {path, status: 404});
}
return this.get('session.user').then(user => (
this.store.createRecord(modelName, {authors: [user]})
));
this.store.createRecord(modelName, {authors: [this.session.user]});
},
// there's no specific controller for this route, instead all editor

View File

@ -18,9 +18,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
},
model(params, transition) {

View File

@ -7,9 +7,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
},
setupController(controller) {

View File

@ -7,10 +7,10 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(this.settings.reload());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
return this.settings.reload();
},
actions: {

View File

@ -7,10 +7,10 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(this.settings.reload());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
return this.settings.reload();
},
actions: {

View File

@ -7,10 +7,10 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(this.settings.reload());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
return this.settings.reload();
},
actions: {

View File

@ -7,10 +7,10 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(this.settings.reload());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
return this.settings.reload();
},
actions: {

View File

@ -24,10 +24,9 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionDisabled())
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionDisabled();
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
},
model(params, transition) {

View File

@ -6,10 +6,8 @@ export default class LaunchRoute extends AuthenticatedRoute {
beforeModel() {
super.beforeModel(...arguments);
return this.session.user.then((user) => {
if (!user.isOwner) {
return this.transitionTo('home');
}
});
if (!this.session.user.isOwner) {
return this.transitionTo('home');
}
}
}

View File

@ -18,11 +18,9 @@ export default class MembersRoute extends AuthenticatedRoute {
beforeModel() {
super.beforeModel(...arguments);
return this.session.user.then((user) => {
if (!user.isOwnerOrAdmin) {
return this.transitionTo('home');
}
});
if (!this.session.user.isOwnerOrAdmin) {
return this.transitionTo('home');
}
}
model(params) {

View File

@ -16,11 +16,9 @@ export default class MembersRoute extends AuthenticatedRoute {
// - logged in user isn't owner/admin
beforeModel() {
super.beforeModel(...arguments);
return this.session.user.then((user) => {
if (!user.isOwnerOrAdmin) {
return this.transitionTo('home');
}
});
if (!this.session.user.isOwnerOrAdmin) {
return this.transitionTo('home');
}
}
model(params) {

View File

@ -38,45 +38,44 @@ export default AuthenticatedRoute.extend({
},
model(params) {
return this.session.user.then((user) => {
let queryParams = {};
let filterParams = {tag: params.tag, visibility: params.visibility};
let paginationParams = {
perPageParam: 'limit',
totalPagesParam: 'meta.pagination.pages'
};
const user = this.session.user;
let queryParams = {};
let filterParams = {tag: params.tag, visibility: params.visibility};
let paginationParams = {
perPageParam: 'limit',
totalPagesParam: 'meta.pagination.pages'
};
assign(filterParams, this._getTypeFilters(params.type));
assign(filterParams, this._getTypeFilters(params.type));
if (params.type === 'featured') {
filterParams.featured = true;
}
if (params.type === 'featured') {
filterParams.featured = true;
}
if (user.isAuthor) {
// authors can only view their own posts
filterParams.authors = user.slug;
} else if (user.isContributor) {
// Contributors can only view their own draft posts
filterParams.authors = user.slug;
filterParams.status = 'draft';
} else if (params.author) {
filterParams.authors = params.author;
}
if (user.isAuthor) {
// authors can only view their own posts
filterParams.authors = user.slug;
} else if (user.isContributor) {
// Contributors can only view their own draft posts
filterParams.authors = user.slug;
filterParams.status = 'draft';
} else if (params.author) {
filterParams.authors = params.author;
}
let filter = this._filterString(filterParams);
if (!isBlank(filter)) {
queryParams.filter = filter;
}
let filter = this._filterString(filterParams);
if (!isBlank(filter)) {
queryParams.filter = filter;
}
if (!isBlank(params.order)) {
queryParams.order = params.order;
}
if (!isBlank(params.order)) {
queryParams.order = params.order;
}
let perPage = this.perPage;
let paginationSettings = assign({perPage, startingPage: 1}, paginationParams, queryParams);
let perPage = this.perPage;
let paginationSettings = assign({perPage, startingPage: 1}, paginationParams, queryParams);
return this.infinity.model(this.modelName, paginationSettings);
});
return this.infinity.model(this.modelName, paginationSettings);
},
// trigger a background load of all tags, authors, and snipps for use in filter dropdowns and card menu
@ -89,13 +88,11 @@ export default AuthenticatedRoute.extend({
});
}
this.session.user.then((user) => {
if (!user.isAuthorOrContributor && !controller._hasLoadedAuthors) {
this.store.query('user', {limit: 'all'}).then(() => {
controller._hasLoadedAuthors = true;
});
}
});
if (!this.session.user.isAuthorOrContributor && !controller._hasLoadedAuthors) {
this.store.query('user', {limit: 'all'}).then(() => {
controller._hasLoadedAuthors = true;
});
}
if (!controller._hasLoadedSnippets) {
this.store.query('snippet', {limit: 'all'}).then(() => {

View File

@ -13,13 +13,11 @@ export default AuthenticatedRoute.extend({
beforeModel(transition) {
this._super(...arguments);
return this.session.user.then((user) => {
if (!user.isOwner) {
return this.transitionTo('home');
}
if (!this.session.user.isOwner) {
return this.transitionTo('home');
}
this.billing.set('previousTransition', transition);
});
this.billing.set('previousTransition', transition);
},
model(params) {

View File

@ -4,8 +4,7 @@ import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
}
});
});

View File

@ -7,9 +7,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
},
model() {

View File

@ -9,9 +9,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
},
model() {

View File

@ -8,9 +8,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor());
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
},
model() {

View File

@ -8,22 +8,21 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel(transition) {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(() => {
if (transition.to.queryParams?.fromAddressUpdate === 'success') {
this.notifications.showAlert(
`Newsletter email address has been updated`.htmlSafe(),
{type: 'success', key: 'members.settings.from-address.updated'}
);
} else if (transition.to.queryParams?.supportAddressUpdate === 'success') {
this.notifications.showAlert(
`Support email address has been updated`.htmlSafe(),
{type: 'success', key: 'members.settings.support-address.updated'}
);
}
});
this.transitionAuthor(this.session.user);
this.transitionEditor(this.session.user);
if (transition.to.queryParams?.fromAddressUpdate === 'success') {
this.notifications.showAlert(
`Newsletter email address has been updated`.htmlSafe(),
{type: 'success', key: 'members.settings.from-address.updated'}
);
} else if (transition.to.queryParams?.supportAddressUpdate === 'success') {
this.notifications.showAlert(
`Support email address has been updated`.htmlSafe(),
{type: 'success', key: 'members.settings.support-address.updated'}
);
}
},
model() {

View File

@ -9,8 +9,7 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor());
this.transitionAuthor(this.session.user);
},
model() {

View File

@ -25,11 +25,9 @@ export default class ProductRoute extends AuthenticatedRoute {
beforeModel() {
super.beforeModel(...arguments);
return this.session.user.then((user) => {
if (!user.isOwnerOrAdmin) {
return this.transitionTo('home');
}
});
if (!this.session.user.isOwnerOrAdmin) {
return this.transitionTo('home');
}
}
setupController(controller, product) {

View File

@ -8,8 +8,7 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor());
this.transitionAuthor(this.session.user);
},
model() {

View File

@ -10,24 +10,24 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
afterModel(user) {
this._super(...arguments);
return this.get('session.user').then((currentUser) => {
let isOwnProfile = user.get('id') === currentUser.get('id');
let isAuthorOrContributor = currentUser.get('isAuthorOrContributor');
let isEditor = currentUser.get('isEditor');
const currentUser = this.session.user;
if (isAuthorOrContributor && !isOwnProfile) {
this.transitionTo('staff.user', currentUser);
} else if (isEditor && !isOwnProfile && !user.get('isAuthorOrContributor')) {
this.transitionTo('staff');
}
let isOwnProfile = user.get('id') === currentUser.get('id');
let isAuthorOrContributor = currentUser.get('isAuthorOrContributor');
let isEditor = currentUser.get('isEditor');
if (isOwnProfile) {
this.store.queryRecord('api-key', {id: 'me'}).then((apiKey) => {
this.controller.set('personalToken', apiKey.id + ':' + apiKey.secret);
this.controller.set('personalTokenRegenerated', false);
});
}
});
if (isAuthorOrContributor && !isOwnProfile) {
this.transitionTo('staff.user', currentUser);
} else if (isEditor && !isOwnProfile && !user.get('isAuthorOrContributor')) {
this.transitionTo('staff');
}
if (isOwnProfile) {
this.store.queryRecord('api-key', {id: 'me'}).then((apiKey) => {
this.controller.set('personalToken', apiKey.id + ':' + apiKey.secret);
this.controller.set('personalTokenRegenerated', false);
});
}
},
serialize(model) {

View File

@ -17,8 +17,7 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor());
this.transitionAuthor(this.session.user);
},
model(params) {

View File

@ -23,8 +23,7 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor());
this.transitionAuthor(this.session.user);
},
// set model to a live array so all tags are shown and created/deleted tags

View File

@ -116,13 +116,13 @@ export default class CustomViewsService extends Service {
}
// eslint-disable-next-line ghost/ember/no-observers
@observes('settings.sharedViews', 'session.isAuthenticated')
@observes('settings.sharedViews', 'session.{isAuthenticated,user}')
async updateViewList() {
let {settings, session} = this;
// avoid fetching user before authenticated otherwise the 403 can fire
// during authentication and cause errors during setup/signin
if (!session.isAuthenticated) {
if (!session.isAuthenticated || !session.user) {
return;
}

View File

@ -1,7 +1,6 @@
import $ from 'jquery';
import Ember from 'ember';
import EmberError from '@ember/error';
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import {computed} from '@ember/object';
import {set} from '@ember/object';
@ -81,11 +80,8 @@ export default Service.extend({
}),
fetch() {
return RSVP.hash({
settings: this.settings.fetch(),
user: this.get('session.user')
}).then(({user}) => {
this.set('_user', user);
return this.settings.fetch().then(() => {
this.set('_user', this.session.user);
return this._setAdminTheme().then(() => true);
});
},

View File

@ -25,7 +25,7 @@ export default class MembersCountCacheService extends Service {
}
async countString(filter = '', {knownCount} = {}) {
const user = await this.session.user;
const user = this.session.user;
const basicFilter = filter.replace(/^subscribed:true\+\((.*)\)$/, '$1');
const filterParts = basicFilter.split(',');

View File

@ -23,16 +23,15 @@ export default class NavigationService extends Service {
}
// eslint-disable-next-line ghost/ember/no-observers
@observes('session.isAuthenticated', 'session.user.accessibility')
@observes('session.{isAuthenticated,user}', 'session.user.accessibility')
async updateSettings() {
// avoid fetching user before authenticated otherwise the 403 can fire
// during authentication and cause errors during setup/signin
if (!this.session.isAuthenticated) {
if (!this.session.isAuthenticated || !this.session.user) {
return;
}
let user = await this.session.user;
let userSettings = JSON.parse(user.get('accessibility')) || {};
let userSettings = JSON.parse(this.session.user.accessibility || '{}') || {};
this.settings = userSettings.navigation || Object.assign({}, DEFAULT_SETTINGS);
}
@ -51,7 +50,7 @@ export default class NavigationService extends Service {
}
async _saveNavigationSettings() {
let user = await this.session.user;
let user = this.session.user;
let userSettings = JSON.parse(user.get('accessibility')) || {};
userSettings.navigation = this.settings;
user.set('accessibility', JSON.stringify(userSettings));

View File

@ -1,40 +1,76 @@
import SessionService from 'ember-simple-auth/services/session';
import {computed} from '@ember/object';
import ESASessionService from 'ember-simple-auth/services/session';
import RSVP from 'rsvp';
import {configureScope} from '@sentry/browser';
import {getOwner} from '@ember/application';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default SessionService.extend({
dataStore: service('store'), // SessionService.store already exists
notifications: service(),
router: service(),
upgradeStatus: service(),
export default class SessionService extends ESASessionService {
@service config;
@service('store') dataStore;
@service feature;
@service notifications;
@service router;
@service settings;
@service upgradeStatus;
@service whatsNew;
user: computed(function () {
return this.dataStore.queryRecord('user', {id: 'me'});
}),
@tracked user = null;
authenticate() {
// ensure any cached this.user value is removed and re-fetched
this.notifyPropertyChange('user');
skipAuthSuccessHandler = false;
return this._super(...arguments);
},
handleAuthentication() {
if (this.skipAuthSuccessHandler) {
async populateUser(options = {}) {
if (this.user) {
return;
}
// standard ESA post-sign-in redirect
this._super('home');
const id = options.id || 'me';
const user = await this.dataStore.queryRecord('user', {id});
this.user = user;
}
// trigger post-sign-in background behaviour
this.user.then(() => {
this.notifications.clearAll();
this.loadServerNotifications();
});
},
async postAuthPreparation() {
await RSVP.all([
this.config.fetchAuthenticated(),
this.feature.fetch(),
this.settings.fetch()
]);
// update Sentry with the full Ghost version which we only get after authentication
if (this.config.get('sentry_dsn')) {
configureScope((scope) => {
scope.addEventProcessor((event) => {
return new Promise((resolve) => {
resolve({
...event,
release: `ghost@${this.config.get('version')}`
});
});
});
});
}
this.loadServerNotifications();
this.whatsNew.fetchLatest.perform();
}
async handleAuthentication() {
try {
await this.populateUser();
} catch (err) {
await this.invalidate();
}
await this.postAuthPreparation();
if (this.skipAuthSuccessHandler) {
this.skipAuthSuccessHandler = false;
return;
}
super.handleAuthentication('home');
}
handleInvalidation() {
let transition = this.appLoadTransition;
@ -44,28 +80,26 @@ export default SessionService.extend({
} else {
run.scheduleOnce('routerTransitions', this, 'triggerAuthorizationFailed');
}
},
}
// TODO: this feels hacky, find a better way than using .send
triggerAuthorizationFailed() {
getOwner(this).lookup(`route:${this.router.currentRouteName}`).send('authorizationFailed');
},
}
loadServerNotifications() {
if (this.isAuthenticated) {
this.user.then((user) => {
if (!user.isAuthorOrContributor) {
this.dataStore.findAll('notification', {reload: true}).then((serverNotifications) => {
serverNotifications.forEach((notification) => {
if (notification.top || notification.custom) {
this.notifications.handleNotification(notification);
} else {
this.upgradeStatus.handleUpgradeNotification(notification);
}
});
if (!this.user.isAuthorOrContributor) {
this.dataStore.findAll('notification', {reload: true}).then((serverNotifications) => {
serverNotifications.forEach((notification) => {
if (notification.top || notification.custom) {
this.notifications.handleNotification(notification);
} else {
this.upgradeStatus.handleUpgradeNotification(notification);
}
});
}
});
});
}
}
}
});
}

View File

@ -12,16 +12,13 @@ export default EphemeralStore.extend({
// session cookie or not so we can use that as an indication of the session
// being authenticated
restore() {
return this.session.user.then(() => {
return this.session.populateUser().then(() => {
// provide the necessary data for internal-session to mark the
// session as authenticated
let data = {authenticated: {authenticator: 'authenticator:cookie'}};
this.persist(data);
return data;
}).catch(() => {
// ensure the session.user doesn't return the same rejected promise
// after a succussful login
this.session.notifyPropertyChange('user');
return RSVP.reject();
});
}

View File

@ -84,12 +84,15 @@ describe('Integration: Service: feature', function () {
server.shutdown();
});
it('loads labs and user settings correctly', function () {
it('loads labs and user settings correctly', async function () {
stubSettings(server, {testFlag: true});
stubUser(server, {testUserFlag: true});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
return service.fetch().then(() => {
@ -98,12 +101,15 @@ describe('Integration: Service: feature', function () {
});
});
it('returns false for set flag with config false and labs false', function () {
it('returns false for set flag with config false and labs false', async function () {
stubSettings(server, {testFlag: false});
stubUser(server, {});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
@ -113,12 +119,15 @@ describe('Integration: Service: feature', function () {
});
});
it('returns true for set flag with config true and labs false', function () {
it('returns true for set flag with config true and labs false', async function () {
stubSettings(server, {testFlag: false});
stubUser(server, {});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', true);
@ -128,12 +137,15 @@ describe('Integration: Service: feature', function () {
});
});
it('returns true for set flag with config false and labs true', function () {
it('returns true for set flag with config false and labs true', async function () {
stubSettings(server, {testFlag: true});
stubUser(server, {});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
@ -143,12 +155,15 @@ describe('Integration: Service: feature', function () {
});
});
it('returns true for set flag with config true and labs true', function () {
it('returns true for set flag with config true and labs true', async function () {
stubSettings(server, {testFlag: true});
stubUser(server, {});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', true);
@ -158,12 +173,15 @@ describe('Integration: Service: feature', function () {
});
});
it('returns false for set flag with accessibility false', function () {
it('returns false for set flag with accessibility false', async function () {
stubSettings(server, {});
stubUser(server, {testUserFlag: false});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
return service.fetch().then(() => {
@ -172,12 +190,15 @@ describe('Integration: Service: feature', function () {
});
});
it('returns true for set flag with accessibility true', function () {
it('returns true for set flag with accessibility true', async function () {
stubSettings(server, {});
stubUser(server, {testUserFlag: true});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
return service.fetch().then(() => {
@ -186,12 +207,15 @@ describe('Integration: Service: feature', function () {
});
});
it('saves labs setting correctly', function () {
it('saves labs setting correctly', async function () {
stubSettings(server, {testFlag: false});
stubUser(server, {testUserFlag: false});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
@ -209,12 +233,15 @@ describe('Integration: Service: feature', function () {
});
});
it('saves accessibility setting correctly', function () {
it('saves accessibility setting correctly', async function () {
stubSettings(server, {});
stubUser(server, {testUserFlag: false});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
return service.fetch().then(() => {
@ -231,12 +258,15 @@ describe('Integration: Service: feature', function () {
});
});
it('notifies for server errors on labs save', function () {
it('notifies for server errors on labs save', async function () {
stubSettings(server, {testFlag: false}, false);
stubUser(server, {});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
@ -263,12 +293,15 @@ describe('Integration: Service: feature', function () {
});
});
it('notifies for server errors on accessibility save', function () {
it('notifies for server errors on accessibility save', async function () {
stubSettings(server, {});
stubUser(server, {testUserFlag: false}, false);
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
return service.fetch().then(() => {
@ -294,12 +327,15 @@ describe('Integration: Service: feature', function () {
});
});
it('notifies for validation errors', function () {
it('notifies for validation errors', async function () {
stubSettings(server, {testFlag: false}, true, false);
stubUser(server, {});
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);

View File

@ -61,10 +61,6 @@ describe('Unit: Authenticator: cookie', () => {
let authenticator = this.owner.lookup('authenticator:cookie');
let post = authenticator.ajax.post;
let config = this.owner.lookup('service:config');
let feature = this.owner.lookup('service:feature');
let settings = this.owner.lookup('service:settings');
return authenticator.authenticate('AzureDiamond', 'hunter2').then(() => {
expect(post.args[0][0]).to.equal(`${ghostPaths().apiRoot}/session`);
expect(post.args[0][1]).to.deep.include({
@ -79,11 +75,6 @@ describe('Unit: Authenticator: cookie', () => {
expect(post.args[0][1]).to.deep.include({
contentType: 'application/json;charset=utf-8'
});
// ensure our pre-loading calls have been made
expect(config.fetchAuthenticated.calledOnce, 'config.fetchAuthenticated called').to.be.true;
expect(feature.fetch.calledOnce, 'feature.fetch called').to.be.true;
expect(settings.fetch.calledOnce, 'settings.fetch called').to.be.true;
});
});
});