Ember Route gets stuck after error loading model - javascript

I've run into an annoying issue when loading data asynchronously in an ember route's model callback. The issue seems to be that if the model method of my route returns a promise which is rejected then the route will never attempt to re-evaluate that route model. It just automatically returns the same rejected promise the next time it tries to go to that route without even trying to re-fetch the data!
I understand from this answer that an ember route will only call it's model method when trying to convert the url into a model. I'm guessing that in the case of routes with dynamic segments it may be called if it has never encountered that particular dynamic segment before.
Here is what I've got in my router setup.
window.App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true
});
App.Router.map(function() {
this.route('login');
this.resource('users', { path: '/users' }, function() {
this.resource('user', { path: '/:user_id' });
this.route('create', { path: '/create' });
});
});
And this is my route.
App.UserRoute = Ember.Route.extend({
model: function(params) {
// This returns a promise
return App.User.fetch(params.user_id);
}
});
I have some special handling for errors in my application route so that routes which fail due to authentication exceptions redirect the user to the login screen.
App.ApplicationRoute = Ember.Route.extend({
actions: {
sessionExpired: function() {
this.controllerFor('login').set("tokenExpired", true);
this.transitionTo('login');
},
error: function(err) {
if (err.type === "TokenException") {
this.send('sessionExpired');
}
}
}
});
The Problem
I navigate to the /users route
For some reason my token expires (inactivity, whatever...)
I navigate to the /users/1 route
The route's model method returns a promise which rejects and I am kicked out to the login screen
I log back in and try to navigate back to the /users/1 route
The route automatically just returns the same failed promise it did last time and I'm kicked out to the login screen. :(
I'm thinking that what I want is some way to clear all the evaluated route models after a user logs in. If this was a multi-user system and one user logs out and another user logs in on the same computer without refreshing the page then that new user shouldn't have routes automatically resolved from the previous user's session.
This seems to me like it would be a common problem yet I can't find any sort of app-wide invalidate cache method. How should I solve this?

I'm not sure where ember data stands on the cache clearing feature, but here is one way to do it
clearCache: function (type) {
var map = App.store.typeMapFor(type);
map.idToCid = {};
map.clientIds = [];
map.recordArrays = [];
map.findAllCache = null;
}
And here is an example as to how the ember firebase library handles a fail find using cache clearing.
delete store.typeMapFor(store.modelFor('user')).idToRecord[username];
Full example here:
https://github.com/firebase/emberFire/blob/master/examples/blog/js/app.js

For anyone else who finds this - I never found a way to reset the ember application and cause it to forget all resolved routes. I did find a few other work-arounds.
In the end, I opted to just window.reload() any time that a user logged out of the system or had their authentication token expire.
Authenticated URLs
Another reasonable approach would be to put a random unique id in the hash state. Essentially just do this.
Instead of a route like:
#/contacts/1
prefix every authenticated route with some kind of unique id
#/PyUE4E+JEdOaDAMF6CwzAQ/contacts/1
App.reset
I tried tried a number of things. One of the more promising things I tried was redirecting to the login screen and using the Application's reset method on my global App object. http://emberjs.com/api/classes/Ember.Application.html#method_reset
That didn't work though, it seems that even a reset Application remember's the models of any routes that it has resolved - weird.

Related

Passing unsaved record to Ember.js route

Inside an application we allow users to create new records, related to an existing record. To achieve this, we use actions something like this:
createUser() {
var route = this;
var model = this.store.createRecord('user', {
client: route.modelFor('client'),
});
route.transitionTo('user.update', model);
},
The user.update route renders a user-form component, using the model that was passed in the transition. The same route is also used to update existing users.
The issue with this approach is as follows; when refreshing the page, the page errors because the route fails to find the respective record when querying the store (at this point, the URL is /users/null/update). Ideally I'd pass the client (or client.id) argument in the URL so that:
The page can be reloaded without issue.
The client associated with the user is set correctly.
How can I achieve this in Ember.js? I know that this can easily be done using nested routes (by nesting the user.update route inside a client route), but this doesn't make sense visually.
The relevant parts of the router are as follows:
this.route('clients');
this.route('client', {path: 'clients/:id'}, function() {
this.route('users');
});
this.route('user', {path: 'users/:id'}, function() {
this.route('update');
});
All I do in the user/update.hbs template is {{user-form user=model}}
The problem is that the model you just created has no id at that point because it is not saved, ember can´t route to a model without an id, if possible save the model before you try to transition to the route, if you don´t want to save the model because the user can cancel the action check this thread where a user had the same problem (if I understand you problem correctly), I provided a solution for that problem that I´m using in my own project
https://stackoverflow.com/a/33107273/2214998

Ember.js: redirect in router if certain condition is satisfied in controller?

Basically the objective is render the account page if user is logged in, otherwise redirect to a login page.
I have the following routes:
App.Router.map(function() {
this.resource('account', { path: '/'});
this.route('login', { path: '/login' });
})
My current code tries to access a loggedIn attribute in the account controller in the route:
App.AccountRoute = Ember.Route.extend({
renderTemplate: function(controller) {
var loggedIn = controller.get('loggedIn'); // ERROR: controller undefined
if (!loggedIn) {
this.transitionTo('login');
}
}
});
Should I implement this logic in the router? Why is the controller undefined in my route? Thanks!
Here are a couple ideas that might help you:
Your controller does not always exist. It is created by Ember when it needs it the first time. You can use the Chrome extension for Ember debugging to see which controllers are already created. In your case it should be available though since you are in the renderTemplate hook. In general, redirects should be done either in the beforeModel hook or the redirect hook:
redirect: function () {
if (!this.controller.get('loggedIn')) {
this.transitionTo('login');
}
}
Consider moving the authentication logic into an Ember service (example). A service in Ember is simply a class that extends Ember.Object. You will have the ability to inject that service into all your controllers and routes so it will be always available.
Even better: consider using the excellent ember-simple-auth that handles both authentication and authorization. It will create a session service available everywhere in your app, so you will be able to do things such as:
// Ensures the user is authenticated
if (!this.get('session.isAuthenticated')) {
this.transitionTo('login');
}
Or even better (since you don't want to copy paste that stuff everywhere):
// This route is now authenticated!
App.AccountRoute = Ember.Route.extend(AuthenticatedRouteMixin, {
...
}
And many other cool things!
Also, I see that you are not using Ember CLI yet. I'd recommend it once you feel more comfortable with Ember. Ember CLI is the future of Ember, it comes with a slightly different syntax but lot of great things.

prevent users to navigate back to specific routes with backbone

I am new to backbone and I want to implement a very simple auth using backbone router.
I am actually using only the router from backbone in my app. When I start the app I render a login view and I also init the backbone router (Backbone.history.start();)
If login succeeded I call router.navigate('mainmenu', { trigger: true, replace: false }); to navigate to a new route where I render the main menu, but when I click on the browser's back button I navigate back to the login view.
Before navigating to the previous view (the login view) I want to ask the user if he wants to logout first, and if logout process goes well, then he is redirected to the login view.
How can I achieve that? I checked few other questions, but the answer is too complicated for my use case. I just want to prevent users to navigate back to specific views if they're logged in.
#Dethariel thanks for the answer. I successfully implemented some kind of session, using the built-in Backbone router. I started with their small example snippet from the Backbone.Router execute method backbone router execute snippet and did something similar to bellow:
var Router = Backbone.Router.extend({
// define routes and calkbacks
// ....
// define routes and calkbacks
execute: function(callback, args) {
// execute will be called before the callback for each specific route
// get the next route in here
var nextRoute = Backbone.history.fragment;
if(user.LoggedIn()){
// check if nextRoute is '#login*'. I could make other checks as well
if(nextRoute.indexOf('login')>-1)
prompt('Log out?');
// else continue routing
else if (callback) callback.apply(this, args);
}
else if (callback) callback.apply(this, args);
}
});
This is very minimal, and I don't think it's the best or secure way, but it's a good starting point for me.
You can add a backbone route which will handle the login page (if you haven't done that yet). Once this route is hit, you do (pseudo-code follows):
if (user.isLoggedIn()) {
if (showLogoutPrompt().decision === "logout") {
user.logout();
}
}
Hope this helps.

Accessing the error model in Ember ErrorRoute

I'm trying to write a handler for all failed routes in my Ember application.
The documentation here seems to suggest that I can make a ErrorRoute on my App object which will automatically be transitioned to when routing on another route failed. I want to use this to redirect the user to a login page if the reason for the routing failure is due to an authentication problem (such as token timeout).
The problem I have is that inside the ErrorRoute I don't seem to have any way to access the error returned by the route that failed. I want to check for sure that it was an authentication error before redirecting them to the login screen and I'm not sure how to do that.
Here is what I wrote for testing:
App.ErrorRoute = Ember.Route.extend({
activate: function() {
console.log("MODEL A", this.modelFor('error'));
setInterval(function() {
console.log("MODEL B", this.modelFor('error'));
}.bind(this), 1000);
}
});
When the route is activated, the console logs MODEL A undefined as it tries to access the model for the App.ErrorController which isn't set yet for some reason. After a second the MODEL B console log fires and the error model has been set up.
If I can't access the error in the Route's activate method then where can I access it? Presumably I'm not supposed to wrap my logic in a timeout.
You could manage the error in any route of your current active hierarchy.
Normally, you setup your error handler at the application route to perform your app error logic.
App.AccountRoute = Ember.Route.extend({
afterModel: function() {
return new Em.RSVP.Promise(function(resolve, reject){
throw new AuthenticatedError('error message');
});
}
});
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(error) {
if ( error instanceof AuthenticatedError ) {
this.transitionTo('login');
} else {
// if return true, the event will bubble and transition to error
return true;
}
}
}
});
http://emberjs.jsbin.com/cisur/1/edit
The error model is passed to the error route's renderTemplate(controller, model) method.
If you don't care about landing on a *.error route substate, if you're going to redirect anyway, #ppcano's answer is sufficient. But sometimes you want to encapsulate all error handling in a dedicated error route object. renderTemplate then is your place to handle things.
I agree though, it would be nice if its hooks had the correct modelFor(...) available.

Resolve $http request before running app and switching to a route or state

I have written an app where I need to retrieve the currently logged in user's info when the application runs, before routing is handled. I use ui-router to support multiple/nested views and provide richer, stateful routing.
When a user logs in, they may store a cookie representing their auth token. I include that token with a call to a service to retrieve the user's info, which includes what groups they belong to. The resulting identity is then set in a service, where it can be retrieved and used in the rest of the application. More importantly, the router will use that identity to make sure they are logged in and belong to the appropriate group before transitioning them to the requested state.
I have code something like this:
app
.config(['$stateProvider', function($stateProvider) {
// two states; one is the protected main content, the other is the sign-in screen
$stateProvider
.state('main', {
url: '/',
data: {
roles: ['Customer', 'Staff', 'Admin']
},
views: {} // omitted
})
.state('account.signin', {
url: '/signin',
views: {} // omitted
});
}])
.run(['$rootScope', '$state', '$http', 'authority', 'principal', function($rootScope, $state, $http, authority, principal) {
$rootScope.$on('$stateChangeStart', function (event, toState) { // listen for when trying to transition states...
var isAuthenticated = principal.isAuthenticated(); // check if the user is logged in
if (!toState.data.roles || toState.data.roles.length == 0) return; // short circuit if the state has no role restrictions
if (!principal.isInAnyRole(toState.data.roles)) { // checks to see what roles the principal is a member of
event.preventDefault(); // role check failed, so...
if (isAuthenticated) $state.go('account.accessdenied'); // tell them they are accessing restricted feature
else $state.go('account.signin'); // or they simply aren't logged in yet
}
});
$http.get('/svc/account/identity') // now, looks up the current principal
.success(function(data) {
authority.authorize(data); // and then stores the principal in the service (which can be injected by requiring "principal" dependency, seen above)
}); // this does its job, but I need it to finish before responding to any routes/states
}]);
It all works as expected if I log in, navigate around, log out, etc. The issue is that if I refresh or drop on a URL while I am logged in, I get sent to the signin screen because the identity service call has not finished before the state changes. After that call completes, though, I could feasibly continue working as expected if there is a link or something to- for example- the main state, so I'm almost there.
I am aware that you can make states wait to resolve parameters before transitioning, but I'm not sure how to proceed.
OK, after much hair pulling, here is what I figured out.
As you might expect, resolve is the appropriate place to initiate any async calls and ensure they complete before the state is transitioned to.
You will want to make an abstract parent state for all states that ensures your resolve takes place, that way if someone refreshes the browser, your async resolution still happens and your authentication works properly. You can use the parent property on a state to make another state that would otherwise not be inherited by naming/dot notation. This helps in preventing your state names from becoming unmanageable.
While you can inject whatever services you need into your resolve, you can't access the toState or toStateParams of the state it is trying to transition to. However, the $stateChangeStart event will happen before your resolve is resolved. So, you can copy toState and toStateParams from the event args to your $rootScope, and inject $rootScope into your resolve function. Now you can access the state and params it is trying to transition to.
Once you have resolved your resource(s), you can use promises to do your authorization check, and if it fails, use $state.go() to send them to the login page, or do whatever you need to do. There is a caveat to that, of course.
Once resolve is done in the parent state, ui-router won't resolve it again. That means your security check won't occur! Argh! The solution to this is to have a two-part check. Once in resolve as we've already discussed. The second time is in the $stateChangeStart event. The key here is to check and see if the resource(s) are resolved. If they are, do the same security check you did in resolve but in the event. if the resource(s) are not resolved, then the check in resolve will pick it up. To pull this off, you need to manage your resources within a service so you can appropriately manage state.
Some other misc. notes:
Don't bother trying to cram all of the authz logic into $stateChangeStart. While you can prevent the event and do your async resolution (which effectively stops the change until you are ready), and then try and resume the state change in your promise success handler, there are some issues preventing that from working properly.
You can't change states in the current state's onEnter method.
This plunk is a working example.
We hit a similar issue. We felt we needed to make the logic which makes the HTTP
call accessible to the logic that handles the response. They're separate in the
code, so a service is a good way to do this.
We resolved that separation by wrapping the $http.get call in a service which
caches the response and calls the success callback immediately if the cache is
already populated. E.g.
app.service('authorizationService', ['authority', function (authority) {
var requestData = undefined;
return {
get: function (successCallback) {
if (typeof requestData !== 'undefined') {
successCallback(requestData);
}
else {
$http.get('/svc/account/identity').success(function (data) {
requestData = data;
successCallback(data);
});
}
}
};
}]);
This acts as a guard around the request being successful. You could then call authorizationService.get() within your $stateChangeStart
handler safely.
This approach is vulnerable to a race condition if a request is in already progress when authorizationService.get() is called. It might be possible to introduce some XHR bookkeeping to prevent that.
Alternatively you could publish a custom event once the HTTP request has
completed and register a subscriber for that event within the
$stateChangeStart handler. You would need to unregister that handler later
though, perhaps in a $stateChangeEnd handler.
None of this can complete until the authorisation is done, so you should inform
the user that they need to wait by showing a loading view.
Also, there's some interesting discussion of authentication with ui-router in
How to Filter Routes? on the AngularJS Google Group.
I think a cleaner way to handle this situation is t$urlRouterProvider. deferIntercept() together with $urlRouter.listen() in order to stop uiRouter until some data is retrieved from server.
See this answer and docs for more information.

Categories

Resources