User loading after authentication with ember simple auth - ember.js

I use ember simple auth to authenticate my users.
It workls perfectly fine except when the user just logged in.
I have an helper that ask for the current user :
import Helper from '#ember/component/helper';
import { inject as service } from '#ember/service';
export default Helper.extend({
currentUser: service('current-user'),
compute([feature,currentUser]) {
let role = currentUser.get('user.role')
Helper is used in a template embedded in a template in application.hbs
{{#if (permission-for "users" currentUser)}}
<li>{{#link-to 'users'}}<i class="fa fa-user"></i> <span>Clients</span>{{/link-to}}</li>
{{/if}}
The current user service is a duplicate of this one from the doc : https://github.com/simplabs/ember-simple-auth/blob/master/guides/managing-current-user.md#loading-the-user-with-its-id
And my application.js is also using the code from the same guide.
From what I tried, the sessionAuthenticated start to run, and during await this._loadCurrentUser(); the helper is executed, and it finish to run sessionAuthenticated
What happen is that the user attribute I want is undefined and it make it impossible to display interface elements according to the role of the User.
How can I fix this problem?
Edit for related question :How does ember simple auth is supposed to be used in template to determine if User is supposed to see some part of the interface or not ? Like for example menu choices.

The problem with your approach is the helper is being executed synchronously and isn't "watching" the currentUser service in a way that would recompute. It would be a lot easier to use a computed property to do what you want:
controllers/application.js
currentUser: service(),
hasPermissionForUsers: computed("currentUser.user", function() {
const user = this.get("currentUser.user");
if (! user) return false;
return user.get("role") === "users"; // taking a random guess at what your helper does here
})
This property will now re-compute once the user is loaded:
application.hbs
{{#if hasPermissionForUsers}}
<li>{{#link-to 'users'}}<i class="fa fa-user"></i> <span>Clients</span>{{/link-to}}</li>
{{/if}}
If you have to use a helper, it is doable but it's not pretty. The reason it's not as elegant is because helpers don't have access to the same context so it's more difficult to recompute. You could do something like this:
import Helper from '#ember/component/helper';
import { inject as service } from '#ember/service';
import { run } from '#ember/runloop';
import { addObserver, removeObserver } from '#ember/object/observers';
export default Helper.extend({
currentUser: service(),
willDestroy() {
this._super(...arguments);
const container = this.get('_container');
removeObserver(container, 'currentUser.user', this, this.update);
},
compute([feature, container]) {
addObserver(container, 'currentUser.user', this, this.update);
this.set('_container', container); // used for cleanup
const user = this.get('currentUser.user');
if (! user) return false;
return user.get('role') === feature;
},
update() {
run.once(this, this.recompute);
}
});
Then use it like so:
application.hbs
{{#if (permission-for "users" this)}}
<li>{{#link-to 'users'}}<i class="fa fa-user"></i> <span>Clients</span>{{/link-to}}</li>
{{/if}}

First I would recommend to not pass the currentUser service to the helper if you inject it to the service.
Next you can let the helper recompute after the user was loaded:
export default Helper.extend({
currentUser: service('current-user'),
init() {
if(!this.currentUser.user) {
this.currentUser.runAfterLoaded.push(() => this.recompute());
}
},
compute([feature]) {
if(!this.currentUser.user)
return false;
}
let role = this.currentUser.get('user.role')
...
return ..;
}
});
you'll also want to add this.runAfterLoaded.forEach(x => x()) to the load() function in the current-user service after the this.set('user', user); as well as this.set('runAfterLoaded', []) to the init hook.
Another possibility is to build a mixin that you add to the relevant routes after the AuthenticatedRouteMixin that ensures the current user will be loaded.

Related

EmberJS 2.7 - infinity-model add-on inside a component - how to 'bubble up' the infinityLoad action

I am using the awesome plugin infinity-loader. It works great, providing I use it on the template of the route it is bound to.
But like a lot of people, now I decided to try and use it on a component. Oh dear. I understand how to bubble/send actions like this fiddle shows and this question explains.
Sadly this, this and this did not help.
What is strange is that after the first page, the Infinity add-on fires the action infinityLoad - if I remove my handler in my component, then I see the error 'nothing handled' the action in the console, so I know that the action is firing when I scroll to the end of the list.
But when my component 'bubbles it up' it just seems to get swallowed in the route and does not cause the add-on to fire its own internal handler.
/templates/employees/index.hbs
{{employees-list employeeModel=model sendThisAction='infinityLoad'}}
/routes/employees/index.js
import Ember from 'ember';
import InfinityRoute from "ember-infinity/mixins/route";
export default Ember.Route.extend(InfinityRoute, {
totalPagesParam: "meta.total",
model() {
return this.infinityModel("employee", { perPage: 10, startingPage: 1 });
},
// is this really needed, because my understanding is this action is automatically handled by the addon?
actions: {
infinityLoad() {
this.get('infinityModel').loadMore();
}
}
});
/templates/components/employee.hbs
<div class="list-group">
{{#each employeeModel as |employee|}}
<...display some employee stuff ...>
{{/each}}
</div>
{{infinity-loader infinityModel=employeeModel}}
/components/employee-list.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
infinityLoad() {
this.sendAction('sendThisAction');
}
}
});
NOTE
I also tried solving this problem using this great add-on from Dockyard for sending actions to routes. The add-on works, but not my code.
UPDATE
With the code below the event now bubbles up and when I scroll the page I get the alert. But now I need to figure out how to get the loader (which is basically the grandchild of the route) to load the next page.
/templates/employees/index.hbs
{{employees-list employeeModel=model infinityLoad='infinityLoad'}}
/routes/employees/index.js
import Ember from 'ember';
import InfinityRoute from "ember-infinity/mixins/route";
export default Ember.Route.extend(InfinityRoute, {
totalPagesParam: "meta.total",
model() {
return this.infinityModel("employee", { perPage: 10, startingPage: 1 });
},
// is this really needed, because my understanding is this action is automatically handled by the addon?
actions: {
infinityLoad() {
alert('here we are'); <- test
}
}
});
/templates/components/employee.hbs
<div class="list-group">
{{#each employeeModel as |employee|}}
<...display some employee stuff ...>
{{/each}}
</div>
{{infinity-loader infinityModel=employeeModel}}
/components/employee-list.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
infinityLoad() {
this.sendAction('infinityLoad');
}
}
});
This helped figure that part out.
So finally with some input from the developer of the infinity-loader component, this is what is needed to use it in a component:
app/components/employee-list.js:
infinityLoadAction: 'infinityLoad',
actions: {
infinityLoad(employeeModel) {
this.sendAction('infinityLoadAction', employeeModel);
}
}
The infinityLoad is fired automatically when you have the loader in your template. You just have to 'catch and throw' it like above.
Also, you don't need to add any code in your route to catch it, because the infinity-loader will catch the infinityLoadAction that you throw from the component.
I Assume you can solve your problem with changing
{{employees-list employeeModel=model sendThisAction='infinityLoad'}}
to
{{employees-list employeeModel=model action='infinityLoad'}}
and changing this
actions: {
infinityLoad() {
this.sendAction('sendThisAction');
}
}
to
actions: {
infinityLoad() {
this.sendAction('infinityLoad');
}
}
I am also suspect to this
infinityLoad() {
this.get('infinityModel').loadMore();
}
to change with
infinityLoad() {
this.loadMore();
}
let's give it a try, as I don't have your code I cannot debug but based on fact on component and how we can pass that to route it should work hopefully.
if it's not working let me know or maybe you can share a live version.

How to set an application wide variable? (on app controller?)

I am using the latest Ember, 2.1, and am wondering how to set some application wide variables, such as user id, username, email, etc, presumably on the application controller.
While ember applications have tons of files, I haven't really done a lot yet, I'm somewhat confident I'm sharing just the right code. I don't have a login route file. I have the ember simple auth plugin installed, but I'm not actually using/invoking it any special way, except for mixing it into my application route:
import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin';
export default Ember.Route.extend(ApplicationRouteMixin)
My router:
this.route('login')
My login template:
<button {{action 'forceLogin'}}>Force login of devinrhode2#gmail.com by clicking this action button</button>
<p>Your account id is: {{account_id}}</p>
My login controller:
export default Ember.Controller.extend({
actions: {
forceLogin: () => {
var data = {
"account_id": 123,
"email":"devinrhode2#gmail.com",
"name":"Devin Rhode"
}
this.setProperties(data)
}
}
});
I have the forceLogin controller action being called, but, the {{account_id}} is not populating into the template. How could I get the account_id to render back into the template? How could I make the account_id globally accessible to my ember application by calling this.get('account_id') wherever I need it?
Currently I get the error:
Cannot read property 'setProperties' of undefined
You get the error because of the way you define forceLogin. Arrow functions are bound to the context where they're defined. Here's what your code compiles to:
var _this = this; // we capture `this` from out here!
export default Ember.Controller.extend({
actions: {
forceLogin() {
...
_this.setProperties(data) // `_this` is the window!
}
}
});
That's no good because this should be the instance of the controller and instead it's the window.
Instead you should define forceLogin like this:
export default Ember.Controller.extend({
actions: {
forceLogin() {
...
this.setProperties(data) // `this` is our controller instance
}
}
});
To get the account_id from somewhere else, you can inject the login controller:
// in some other controller
export default Ember.Controller.extend({
login: Ember.inject.controller(),
actions: {
doSomethingWithTheAccountId() {
var accountId = this.get('login.account_id');
...
}
}
});
It would be cleaner to move those properties to a service, which you can inject anywhere with Ember.inject.service()

How to call an action on the app route from any component without having to pass it everywhere?

I'd like to be able to open a login modal from any component without having to pass it through the component hierarchy each time I need it.
The openModal action on the application is the same as in the modal cookbook in the guides :
App.ApplicationRoute = Ember.Route.extend({
actions: {
openModal: function(modalName) {
return this.render(modalName, {
into: 'application',
outlet: 'modal'
});
}
}
});
I would like to avoid :
{{my-component openModal="openModal" otherArgs=args ... }}
Plus
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
openModal(modalName) {
this.sendAction('openModal', modalName);
}
}
});
I could use a service injected in each component, but then how to call the openModal action on the application route from the service ?
Thanks !
Create a service with a function like this:
openModal(){
this.container.lookup('route:application').send('openModal');
}
Then you only need to inject this service to every component that needs to launch the modal and call this function.

creating controller keeps model from getting to template

I'm building my first ember app (ember-cli), and am confused.
I have the following route, which successfully chooses a view based on an attribute in the model:
import Ember from 'ember';
export default Ember.Route.extend({
afterModel: function(tournament) {
var state = tournament.get('state');
if (state === 0) {
this.transitionTo('tournaments.setup', tournament);
} else if (state === 1) {
this.transitionTo('tournaments.play', tournament);
} else if (state === 2) {
this.transitionTo('tournaments.complete', tournament);
}
}
});
Then I wanted to add some functionality to the tournaments.setup page. So I added a controller (at tournaments/controllers/setup.js:
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
start: function() {
var that = this;
var model = this.get('model');
model.set('state',1);
model.save().then(function(tournament) {
that.transitionToRoute('tournaments.play', tournament);
});
}
}
});
This worked, in the sense of enabling the start action that I had created to change the model and redirect to the desired route. But this also had the effect of keeping the model from making it to the setup.hbs template, shown below:
<h3>{{name}}</h3>
<h4>Setup!</h4>
<p>{{state}}</p>
<p>{{eventDate}}</p>
<button {{action "start"}} class="btn btn-primary">Start</button>
The attributes of the model are shown if the controllers/tournaments/setup.js file does not exist. Somehow creating this file, which is not called when I go to the setup route, prevents the model from reaching the template.
I've also tried explicitly defining the route setup.js route, but that didn't help.
I'm using the FixtureAdapter, if that matters. Any ideas? Is there a concept I'm missing?
If you want the model to be automatically set on your controller, you should extend Ember.ObjectController instead of Ember.Controller in tournaments/controllers/setup.js.

Synchronously inject current user after login

In my EmberJS application, I have a current user initializer, which injects the user into all controllers, routes, and views. It works great when logged in. I need the synchronous loading of the current user object so I can check some user permissions right off the bat.
Here's my initializer:
App.CurrentUserInitializer - Ember.Initializer.extent({
name: 'current-user',
after: 'authentication',
initialize: function(container, app) {
var store = container.lookup('store:main');
app.deferReadiness();
store.find('user', 'me').then(function (user) {
app.register('session:user', user, {instantiate: false});
_.forEach(['route', 'controller', 'view'], function (place) {
app.inject(place, 'currentUser', 'session:user');
});
app.advanceReadiness();
}).catch(function () {
app.advanceReadiness();
});
}
});
Where this breaks down for me, is during login. When the app boots, the initializer is run, but the /users/me route returns a 401 error. If I don't catch the error and advanceReadiness, booting halts. By catching the error, the app boots, but the initializer isn't run again after login, so the current user isn't loaded.
What are my options here? I can't use #marcoow's recommended method of adding a computed property to the Session, as I need the user loaded at boot time.
I've tried forcing loading the user object on the IndexRoute as a hack, but that doesn't seem to work.
Any tips are appreciated.
I'd register a session:current object with user property being null. That would be injected into controllers and routes (not sure injecting inside views is a good idea).
So at boot time the user is unknown, but the user lookup is done before the router goes deeper than application route, the root:
In the beforeModel hook of the application route, you'll load that current user. Then:
either you got the user and you set it this.set('session.user', model)
or you'll go in the error hook of the application route, in which case you'd have to check why, and if 401 then you can redirect the user to the login route this.transitionTo('login')
Don't forget to set a flag on session if you got the 401 so that transitionTo will make our user lookup of beforeModel to not happen again until we reach the login route
The code to be used to load the session user and initialise it could be placed in that session:current object so that you could call it from the application route or the login controller.
This is for example my session initialiser (not exactly as I explained, but loading in the initialiser, so closer to what you hve). I used a session model so that I do /session/current and then got a user into it (or not) which has the correct id and not me which then would make the store to load the same user with another id, and so have twice the same user as 2 different records:
app/models/session.js:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
user: DS.belongsTo('user'),
isAuthenticated: Ember.computed.bool('user.isClaimed')
});
app/initializers/session.js:
import Ember from 'ember';
export default {
name: 'session',
after: 'store',
initialize: function (container, app) {
var store = container.lookup('store:main'),
sid = Ember.$.cookie('sid');
// used to register a session
var register = function (session) {
app.register('session:main', session, {instantiate: false});
app.inject('route', 'session', 'session:main');
app.inject('controller', 'session', 'session:main');
};
// used to create a new session and trigger the backend to get details about it
// useful if the server is able to give an existing session while the browser doesn't know about it
// with external providers for example
var newSession = function () {
var session = store.createRecord('session');
// be sure to wipe out any invalid session ID
Ember.$.removeCookie('sid');
register(session);
return session.save().then(function (model) {
// if we got a valid new session, save its ID
Ember.$.cookie('sid', model.get('id'));
}).catch(function () {
Ember.debug('error saving new session: ' + Array.prototype.join.call(arguments, ', '));
});
};
// overall logic ==================
app.deferReadiness();
if (sid) {
// try to load the existing session
store.find('session', sid).then(function (model) {
register(model);
app.advanceReadiness();
}).catch(function () {
// there was a cookie for the session but it might have expired or the server invalidated it
Ember.debug('error loading session: ' + Array.prototype.join.call(arguments, ', '));
newSession().finally(function () {
app.advanceReadiness();
});
});
}
else {
// we don't have any trace of a session, let's just create a new one
newSession().finally(function () {
app.advanceReadiness();
});
}
}
};
app/router.js:
import Ember from 'ember';
var Router = Ember.Router.extend();
Router.map(function () {
this.resource('session', {path: '/'}, function(){
this.route('login');
this.route('logout');
});
});
export default Router;
app/templates/application.hbs (as an example):
<h2 id='title'>Welcome to my app</h2>
{{#if session.isAuthenticated}}
<a {{action 'session.logout'}}>Logout</a>
{{else}}
{{#link-to 'session.login'}}Login{{/link-to}}
{{/if}}
{{outlet}}
Then once in login controller, when the user actually logs in, the server will return the session model with the user linked into it, so the Ember binding magic will just update the session object.