I have a very simple app showing a login screen and a dashboard. When the login is successful the app navigates to the dashboard.
In this state, a back navigation (android) should not go back to the login screen.
This all works fine using the IonicHistory
this.$ionicHistory.nextViewOptions({
disableBack: true
});
return this.$state.go("dashboard");
This is all done in my LoginCtrl when the SessionService (which does the login call to the service and the Session management...) resolves the returned promise.
Now I want to write a Unit-Test to check this behaviour and make sure, never every removes this nextViewOptions-call.
My idea is something like this:
beforeEach(() => {
angular.mock.module("ionic");
angular.mock.module("myapp");
inject(function ($q: IQService, $rootScope: IScope, $ionicHistory: IonicHistoryService): void {
qService = $q;
scope = $rootScope;
ionicHistory = $ionicHistory;
});
});
describe("login", () => {
// Stub my sessionservice to immediatly resolve instead of doing a request
sinon.stub(sessionService, "login", () => qService.when());
let loginCtrl: LoginCtrl = new LoginCtrl(sessionService, stateService, ionicHistory);
loginCtrl.executeLogin();
scope.$apply();
// Here I want to validate, that the history (and therefore the back-stack) is correct.
// But the ionicHistory.viewHistory() does not change at all.
// Independent whether I add the nextViewOptions stuff or not...
}
Here the output from ionichHistory.viewHistory().histories:
Object{root: Object{historyId: 'root', parentHistoryId: null, stack: [], cursor: -1}}
I use the karma test runner to execute the tests.
I'm using:
Ionic 1.3.0
Karama Test Runner
TypeScript
Is this actually possible at all? Do I miss something completely?
In my app I have a situation in which based on a certain condition, if a user access a certain route ('/checkinsuccess') before that controller and view loads a function gets run that checks if a certain flag is true using routeProviders resolve property. If this flag is true it redirects to a new route. Unfortunately it seems like the initial view and controller that I'm trying to redirect away from ('/checkinsuccess') are loading before the function being called in the resolve property finishes. This is causing issue since that initial view runs an auto logout function. Is there a way to run the check before the initial route loads, I thought that's what resolve did? Here's a piece of my code.
.when('/checkinsuccess', {
controller: 'thankuctrl',
templateUrl: 'views/MobileCheckin/checkin-success.html',
resolve: {
checkForWorkflowUpdates: function($location){
checkForWorkflowUpdates($location)
}
}
// check if workflow update has been made and load that route if true.
function checkForWorkflowUpdates($location){
var selectedForms = JSON.parse(sessionStorage.getItem('selectedForms'));
if (selectedForms) {
if(selectedForms.workersComp){
$location.path('/workerscomp');
} else if (selectedForms.motorVehical) {
$location.path('/autoaccident');
}
}
}
I am using the following code to resolve a resource when the main state is loaded. Is it possible to re - resolve the resource without reloading the page? I am avoiding reload so that the user experience is not affected.
$stateProvider
.state('main', {
url: '/',
templateUrl: 'publicApp/main.html',
controller: 'MainCtrl as mainCtrl',
resolve: {
userData: ["UserApi", function (UserApi) {
return UserApi.getUserData().$promise;
}]
}
})
.controller('MainCtrl', function (userData) {
console.log(userData.something);
})
Since is a public site, the user can visit any page without logging in but when the user logs in the page must customize based on the user data.
EDIT
I am using a modal for login so the state doesn't reload after logging in, I was thinking of throwing an event on $rootScope and then add listeners in controllers to load them again. But this doesn't look good, so I am looking for a better way
I currently have two options:
Reload page - will effect the user experience, so its the last option
Throw an event for the login modal and catch it in other controllers
Any better ideas?
Try using state reload once promise is resolved
$state.reload();
currently i'm developing a sample admin application using angularjs, in my application. i have used ui-router instead of ngRoute to define the urls. on my config function states are defined as below.
sampleModule.config(['$stateProvider','$urlRouterProvider',function ($stateProvider,$urlRouterProvider) {
$urlRouterProvider.otherwise('login');
$stateProvider
.state('login',{
url:'/login',
templateUrl: 'views/html/login.html',
controller: 'authCtrl'
}).state('main', {
url:'/main',
templateUrl: 'views/html/main.html',
controller: 'adminViewCtrl'
});
}]);
in the run state of the application i'm redirecting the users to their respective view as follows.
sampleModule.run(function ($rootScope, AuthService, $state) {
$rootScope.$on("$stateChangeStart", function (event) {
if (AuthService.isAuthenticated()) {
event.preventDefault();
$state.go('main');
} else {
event.preventDefault();
$state.go('login');
}
});
});
but the problem i'm facing is application giving following exception and crashes some times when URL changes.
RangeError: Maximum call stack size exceeded
I put a debug point to run function of the application and
and noticed that error coming because content inside if (AuthService.isAuthenticated()) { condition executing in a infinite loop.
below is the image for the errors in chrome developer tools
i'm confused about the things happening here, i Googled for few hours and was not able to come up with a straight answer, i even tried putting event.preventDefault() as some suggestions i saw , but it didn't helped to resolve the problem.
i'm wondering what error i'm doing here? is there any better way to restrict user access to the log-in page,or some other parts of the application after logging in to the application?
The problem here is that you are constantly redirecting user to same state. Here is what happens: user goes to some state, $stateChangeStart is triggered, you redirect user again to some other state, and again $stateChangeStart is triggered, you check authentication and redirect again, and that continues until RangeError appears. What you should probably do is only redirect user when he is unauthorized, if he is authorized, let him see the page he is looking for.
sampleModule.run(function ($rootScope, AuthService, $state) {
$rootScope.$on("$stateChangeStart", function (event, toState) {
if (!AuthService.isAuthenticated() && toState.name !== 'login') {
$state.go('login');
} else if(AuthService.isAuthenticated() && toState.name === 'login') {
$state.go('main');
}
});
});
Here you check if user is authorized and if navigation state is not login (since he already got redirected) and later check if it's login page he is wanting for and redirecting to main
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.