Attempting to get ui-router to resolve the Authentication service before I allow the switch into that state.
unfortunately it continues to evade me, with angular hanging whenever I inject a service that is not part of angular.
I know that I can't inject a service into .config, but surely I should be able to do it just before the controller is loaded?
here's a cut down version of the code:
app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
// For any unmatched url, redirect to state:home
$urlRouterProvider.otherwise("/");
// Now set up the states
$stateProvider
.state('home', {
url: "/",
templateUrl: "partials/home.html",
resolve: {
isAuthorised: ['$log', '$q', 'Auth', function ($log, $q, auth) {
var q = $q.defer();
$log.debug("checking if authorised..."); //this doesn't show
if (auth.whatever()) {
q.resolve(true);
} else {
q.reject(false);
}
return q.promise;
}]
},
controller: "DefaultCtrl"
});
}]);
So the controller never actually gets instantiated... there are no errors in the console.
The same happens if I type any random string in as the service name... so my best guess is that it can't find it? But all other areas of the app can...
I have spent a while researching this and I'm pretty sure I should be able to accomplish this somehow.
any help would be most appreciated.
Grant.
Edit
Thanks Chris T for the Plunkr! I am closing in on the issue, I added your console logs and I'm getting the following error:
Start: {} -> home{}
Error: {} -> home{} ReferenceError: $window is not defined {stack: "ReferenceError: $window is not defined↵ at a (h…pm/planitmoney/claw/dist/js/bower.min.js:815:112)", message: "$window is not defined"}
That bower.min.js file is just angular followed by UI router.. I'm going to do some more checking. Thanks again.
Thanks again to Chris T who created the Plunkr: http://plnkr.co/edit/aeEt4ddUYMM3xRi8M38T?p=preview
which helped solve the issue.
I copied the console logs he has in there over to my application:
// Adds state change hooks; logs to console.
app.run(function($rootScope, $state) {
$rootScope.$state = $state;
function message(to, toP, from, fromP) { return from.name + angular.toJson(fromP) + " -> " + to.name + angular.toJson(toP); }
$rootScope.$on("$stateChangeStart", function(evt, to, toP, from, fromP) { console.log("Start: " + message(to, toP, from, fromP)); });
$rootScope.$on("$stateChangeSuccess", function(evt, to, toP, from, fromP) { console.log("Success: " + message(to, toP, from, fromP)); });
$rootScope.$on("$stateChangeError", function(evt, to, toP, from, fromP, err) { console.log("Error: " + message(to, toP, from, fromP), err); });
});
where I immediately noticed the error:
Start: {} -> home{}
Error: {} -> home{} ReferenceError: $window is not defined {stack: "ReferenceError: $window is not defined↵ at a (h…pm/planitmoney/claw/dist/js/bower.min.js:815:112)", message: "$window is not defined"}
digging a little deeper I traced the error to what was actually throwing it and it was fixed minutes later.
The service that I was attempting to resolve also called in some dependencies of its own. Unknown to me there was a missing $window injection in one of these services.
The Error did not show up in the console
I guess that it may have been because of the unique way in which it was accessed (using ui-routers resolve) Either that or my use of Raven in the application has thrown it somewhere that I didnt see it.
I hope this helps someone else get to the bottom of any issue.
Thanks again everyone.
We have a similar setup that checks localStorage for an token and resolves the promise.
Check the following example:
var requireUser = { User: [ "$location", "$rootScope", "$q", "API", "Session",
function ( $location, $rootScope, $q, API, Session )
{
var deferred = $q.defer();
if( Session.get.authToken() )
{
if( Session.get.user() )
{
deferred.resolve( $rootScope.user );
}
else
{
$location.path( "/login" );
}
}
else
{
deferred.reject( "No user token" );
$location.path( "/login" );
}
return deferred.promise;
} ] };
$routeProvider
.when( "/users", { templateUrl: "/build/templates/users.html", controller: "UsersCtrl", resolve: requireUser, active: "users" } )
Related
As title already suggests, I'm trying to disable some routes. I'm using angular seed project, that already has a nice structure.
I'm using JWT and I'm trying to set up a structure where if a certain route requires user to be logged in, and the user is not logged in, it redirects him to the some other page.
On my angular.module I've added the following code:
.run(['$rootScope', 'userService', '$location', function($rootScope, userService, $location) {
userService.init();
$rootScope.$on('$routeChangeStart', function(event, next, current) {
$rootScope.isPrivate = next['authenticate'];
if ($rootScope.isPrivate) {
if (!userService.get())
$location.path('/');
}
});
}]);
And this is a protected route:
angular.module('myApp.view2', ['ngRoute', 'ngCookies'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/admin/vnos-stevilke', {
templateUrl: 'view2/view2.html',
controller: 'View2Ctrl',
authenticate: true
}).when('/admin/vnos-stevilke/:id', {
templateUrl: 'view2/view2.html',
controller: 'View2Ctrl',
authenticate: true
});
}])
.controller('View2Ctrl', ['$scope', 'webServices', '$location', '$routeParams', function($scope, webServices, $location, $routeParams) {
if ($routeParams.id)
webServices.getBranchById($routeParams.id, function(err, data) {
$scope.branch = data;
});
webServices.getCompanies(function(err, data) {
console.log(data);
console.log('no access!');
if (!err)
$scope.companies = data;
});
}]);
now at first it appears to be working OK: if I'm not logged in, the route is not displayed and I get redirected back to the root. But at a closer look I've noticed that console.log('no access!'); is still displayed in the console. So it appears that controller gets initialized.
It seems like the whole route is loaded and then gets redirected if user is not logged in. That is not the behaviour I'm looking for. I'm trying to HOLD the loading of the route until I'm sure the user is logged in.
Any ideas?
UPDATE:
I changed the code accordingly with the suggestion below, but it doesn't seem to work. Where have I gone wrong?
userService method that checks if user is logged in:
this.isLogged = function() {
var deferred = $q.defer();
if (current === null) return deferred.reject();
else return deferred.resolve(current);
};
Run method:
.run(['$rootScope', 'userService', '$location', function($rootScope, userService, $location) {
userService.init();
$rootScope.$on('$routeChangeError', function() {
$location.path('/');
});
}]);
Restricted page:
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/admin/vnos-stevilke', {
templateUrl: 'view2/view2.html',
controller: 'View2Ctrl',
resolve: function(userService) {
console.log('test');
return userService.isLogged();
}
});
}])
Here, the "test" never displays in console.
You need to just decide if you are letting user into restricted area with resolve route parameter.
If in one of resolve function resolves with a promise object that is rejected it stops entering requested route.
I would write something like:
$routeProvider.when('/restrictedURL',{
...some params,
resolve: function(userService){
return userService.get();
}
}
...and make userService.get return a Promise object that is resolved if session is active and rejected otherwise.
Now.. if promise is rejected a route won't be launched and $routeChangeError event is raised, so you need something like:
angular.module('yourapp').run(function($rootScope){
$rootScope.$on("$routeChangeError",function(){
$location.path('/');
});
});
read more about resolve parameter # https://docs.angularjs.org/api/ngRoute/provider/$routeProvider
Like many folks, I'm new to testing Angular with Jasmine and am struggling to get this right. I use ui-router to do my routing and right now, the problem I'm having is that the $state.current.name in the test is an empty string and I have no idea why it does that.
This is the code in my routing module:
var cacRouteViewMod = angular.module('cacRouteViewMod', ['ui.router', 'cacLib']);
cacRouteViewMod.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('countries', {
url: '/countries',
templateUrl: 'countries/countries.html',
controller: 'countriesCtrl',
resolve : {
countries: ["getCountry", function(getCountry) {
return getCountry();
}]
}
});
}]);
and the test I wrote is this:
describe('cac_app_views (routing)', function() {
var $rootScope,
$state,
$injector,
getCountryMock,
state = 'countries';
beforeEach(function() {
module('cacRouteViewMod', function($provide, $urlRouterProvider) {
$urlRouterProvider.deferIntercept();
$provide.value('getCountry', getCountryMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
$templateCache.put('countries/countries.html', '');
})
});
// Test 1
it('should respond to URL', function() {
expect($state.href(state)).toEqual('#/countries');
});
// Test 2
it('should resolve getCountry', function() {
getCountryMock = jasmine.createSpy('getCountry').and.returnValue('nanana');
$rootScope.$apply(function() {
$state.go('countries');
});
expect($state.current.name).toBe('countries');
expect($injector.invoke($state.current.resolve.countries)).toBe('nanana');
});
});
Test 1 is fine, but test 2 is the issue. The test fails because it expected '' to be 'countries'.
When I log $state.current to the console it gives
Object {name: "", url: "^", views: null, abstract: true}
I'm getting pretty desperate at this point. Could anyone help me understand/solve this problem?
I solved this in this manner:
By reading similar stockoverflow posts, I put a listener for $stateChangeError and it got triggered. I logged out the error data and saw that it's a typeError: getCountry is not a function. This caused the $state not to be updated, and therefore still contains the original(empty) $state.
I fixed the $provide.value to such:
$provide.value('getCountry', getCountryMock = function() {return 'nanana';});
which says "whenever getCountry is called, provide getCountryMock instead, which is a function that returns a string 'nanana'.
Now the tests all work the way I want them to.
Note: I found that getCountryMock = jasmine.createSpy..... line of code to be obsolete with my other change to $provide.value() so I commented it out.
According to the documentation $state.go returns a promise.
You should use done() function from Jasmine in order to test such a code: http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine/
I just started a few days ago with AngularJS and I'm having issues with my interceptor that intercepts 401 statuses from server responses.
It broadcasts a message of the type "loginRequired" when a 401 is returned and a redirect is triggered on that event.
The issue is that if I try to access a restricted page while not being logged in, I can see the page flash for a moment before I'm redirected to the login page. I'm still fairly a beginner in asynchronous stuff, promises etc. Could somebody point out what I'm doing wrong?
Here's my interceptor. As you can see it's really simple but I slimmed it down to explain my point and I'm trying to understand things before developing it further.
The interceptor
var services = angular.module('services', []);
services.factory('myInterceptor', ['$q', '$rootScope',
function($q,$rootScope) {
var myInterceptor = {
'responseError': function(rejection) {
$rootScope.$broadcast('event:loginRequired');
return $q.reject(rejection);
}
};
return myInterceptor;
}
]);
The injection of my interceptor
myApp.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}]);
The route for the restricted page
.when('/restrictedPage', {
templateUrl: 'partials/restrictedPage.html',
controller: 'RestrictedPageController'
}).
The restricted page controller
controllers.controller('RestrictedPageController', function($scope) {
//Some times the alert pops up, sometimes not.
alert("Damn it I shouldn't be there");
});
The $rootScope event watcher
$rootScope.$on('event:loginRequired', function() {
//Only redirect if we aren't on free access page
if ($location.path() == "/freeAccess")
return;
//else go to the login page
$location.path('/home').replace();
});
My issue is clearly with the way I handle the interceptor and $q. I found another way of creating the interceptor on github but it's not the way the official documentation uses, so I think it might be the old way and it's not as clean as putting it in a factory in my opinion. He just puts this code after defining the routes in the config function of his module. But this code works and I don't get the page flash.
Another way I found on Github
var interceptor = ['$rootScope', '$q', '$log',
function(scope, $q, $log) {
function success(response) {
return response;
}
function error(response) {
var status = response.status;
if (status == 401) {
var deferred = $q.defer();
var req = {
config: response.config,
deferred: deferred
};
scope.$broadcast('event:loginRequired');
return deferred.promise;
}
// otherwise
return $q.reject(response);
}
return function(promise) {
return promise.then(success, error);
};
}
];
$httpProvider.responseInterceptors.push(interceptor);
But my goal is not just to "make it work" and I hate the mantra "If it's not broken don't fix it". I want to understand what's the issue with my code. Thanks!
Instead of broadcasting 'event:loginRequired' from your interceptor, try performing the location path change within your interceptor. The broadcast would be increasing the delay between receiving the 401 and changing the location and may be the cause of the screen 'flash'.
services.factory('myInterceptor', ['$q', '$rootScope', '$location',
function($q, $rootScope, $location) {
var myInterceptor = {
'responseError': function(rejection) {
if (response.status === 401 && $location.path() !== '/freeAccess') {
//else go to the login page
$location.path('/home').replace();
}
// otherwise
return $q.reject(response);
}
};
return myInterceptor;
}
]);
You could also perform a HTTP request when your app module first runs to determine right away if the user is authorised:
myApp.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}])
.run(function($http) {
//if this returns 401, your interceptor will be triggered
$http.get('some-endpoint-to-determine-auth');
});
Using AngularJS 1.2.16, ui-router, and Restangular (for API services), I have an abstract state with a child state that uses multiple views. The child view accepts a URL parameter (via $stateParams) and a resolve to fetch the record from an API based on the given ID passed via the URL.
All works perfectly as you navigate throughout the site, however if you are on the given page and refresh it (Command/Ctrl+r or clicking the refresh button) or load the page directly by hitting the deep link with the ID (this is the more crucial problem) the API service is hit by the request but the promise never finishes resolving, thus the data isn't made available to the state, controller, templates, etc.
According to ui-router's documentation, controllers shouldn't be instantiated until all promises are resolved. I've searched high and low in Google and SO, read the AngularJS, Restangular, and ui-router documentation, and a multitude of blogs and tried every iteration I know to figure this out and can't find anything that points to a solution.
Here's the code in question:
company.js (controller code)
angular.module('my.company', ['ui.router', 'CompanySvc'])
.config(['$stateProvider', function config ($stateProvider) {
$stateProvider
.state('company', {
abstract: true,
url: '/company',
data: {
pageTitle: 'Company'
},
views: {
'main': {
templateUrl: 'company/companyMain.tpl.html'
}
}
})
.state('company.landing', {
url: '/{id}',
views: {
'summary': {
controller: 'CompanyCtrl',
templateUrl: 'company/companyDetails.tpl.html'
},
'locations': {
controller: 'CompanyCtrl',
templateUrl: 'company/companyLocations.tpl.html'
}
},
resolve: {
companySvc: 'CompanySvc',
// Retrieve a company's report, if the id is present
company: ['$log', '$stateParams', 'companySvc', function ($log, $stateParams, companySvc) {
$log.info('In company.landing:resolve and $stateParams is: ', $stateParams);
var id = $stateParams.id;
$log.info('In company.landing:resolve and id is: ', id);
return companySvc.getCompany(id).$promise;
// NOTE SO: this was another stab at getting the promise resolved,
// left commented out for reference
/*return companySvc.getCompany(id).then(function (response) {
return response;
});*/
}]
}
});
}
])
.controller('CompanyCtrl', ['$log', '$scope', '$state', 'company',
function CompanyCtrl ($log, $scope, $state, company) {
$log.info('In CompanyCtrl & $state is: ', $state);
$log.info('In CompanyCtrl and company data is: ', company);
$scope.reportData = company ? company : {};
$log.info('In CompanyCtrl and $scope.reportData is: ', $scope.reportData);
}
]);
companyService.js (API service code using Restangular)
angular.module('my.companyService', ['restangular']);
.factory('CompanySvc', ['$log', 'Restangular', function ($log, Restangular) {
var restAngular = Restangular.withConfig(function (Configurer) {
Configurer.setBaseUrl('/int');
});
// Object for working with individual Companies
var _companySvc = restAngular.all('companies');
// Expose our CRUD methods
return {
// GET a single record /int/companies/{id}/report
getCompany: function (id) {
$log.info('In CompanySvc.getCompany & id is: ', id);
return restAngular.one('companies', id).customGET('report').then(function success (response) {
$log.info('Retrieved company: ', response);
return response;
}, function error (reason) {
$log.error('ERROR: retrieving company: ', reason);
return false;
});
}
};
}]);
Some other points that may be added:
* html5mode is true with a hashPrefix
* app is being served by Apache with rewrite rules set
* base href is set
* this is a problem endemic to the entire application where data is being resolved in a state; I just chose this as the example
Finally, here's some output from the console:
In company.landing:resolve and $stateParams is: Object { id="d2c936fb78724880656008a5545b01ea445d4dc4"}
In company.landing:resolve and id is: d2c936fb78724880656008a5545b01ea445d4dc4
In CompanySvc.getCompany & id is: d2c936fb78724880656008a5545b01ea445d4dc4\
In CompanyCtrl & $state is: Object { params={...}, current={...}, $current={...}, more...}
In CompanyCtrl and company data is: undefined
In CompanyCtrl and $scope.reportData is: Object {}
In CompanyCtrl & $state is: Object { params={...}, current={...}, $current={...}, more...}
In CompanyCtrl and company data is: undefined
In CompanyCtrl and $scope.reportData is: Object {}
In CompanyCtrl & $state is: Object { params={...}, current={...}, $current={...}, more...}
Retrieved company: Object { $promise={...}, $resolved=false, route="companies", more...}
Firstly just a note. If you have routing issues (like those with ui-router), instead of using a fiddle, you can just use something like pastebin to provide a single complete demo file that can be run locally, so that the route issues can be tested.
I have created one of those from your fiddle: here
The interesting thing is, that your mock demo seems to work perfectly fine with regards to routing. This makes me think that the issue lies somewhere in your production service. One thing that used to annoy me with ui-router was the difficulty in solving resolve bugs, as there seemed to be little to no error reporting.
If you add this to your apps run function (as I have done in the link above), you should get better resolve error output:
// handle any route related errors (specifically used to check for hidden resolve errors)
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){
console.log(error);
});
I'm attempting to handle http errors within AngularJS (using ui-router), but am losing the context of my run function in the following code.
bugtracker.run(['$rootScope', '$state', function($rootScope, $state) {
//at this point $state and $rootScope are defined
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
//at this point $state and $rootScope are undefined
});
}]);
The code that causes $stateChangeError to trigger is as follows.
//main.js
bugtracker.config(['$stateProvider', '$httpProvider', '$compileProvider', function($stateProvider, $httpProvider, $compileProvider) {
//...
$stateProvider.state('projects.show', {
url: '/{projectId:[0-9]{1,8}}',
templateUrl: '/assets/projects/show.html',
controller: 'ProjectShowCtrl',
resolve: bugtracker.controller('ProjectShowCtrl').resolve
});
//...
}]);
//ProjectShowCtrl.js
projectShowCtrl.resolve = {
project: function(Project, $q, $stateParams, $state) {
var deferred = $q.defer();
Project.findById($stateParams.projectId, function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(errorData); // you could optionally pass error data here
});
return deferred.promise;
},
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
};
I would like $state to be defined within the anonymous function called by the $on function so that I could redirect the user to a 401, 403, etc. page, but I'm unsure why it is not.
In other examples I have seen (https://github.com/angular-ui/ui-router/issues/898) it is implied that $state is defined within the context of the anonymous function.
If anyone could explain why $state is not defined or what I can change to make it defined I would greatly appreciate it!
There's no way these would be undefined, unless they were already undefined in the run block. However I've seen cases where the debugging tool thinks some variables are undefined when they actually aren't. That happened to me in FireBug with an old FF version.