I am trying to do some authentication with AngularUI Router. $urlRouter.sync() looks like exactly what I need. However, that's only available when I intercept $locationChangeSuccess. But when I do that, $state.current.name is empty, whereas I want it to be the current state.
Here's my code so far:
$rootScope.$on('$locationChangeSuccess', function(event, next, nextParams) {
event.preventDefault();
if ($state.current.name === 'login') {
return userService.isAuthenticated().then(function(response) {
var authenticated;
authenticated = response.authenticated;
return alert(authenticated);
});
}
});
Any pointers as to what I'm doing wrong?
I would suggest to go more "UI-Router way". We should use $rootScope.$on('$stateChangeStart' event where $state.current would be properly provided. Here is a working example
Let's observe simple (but not naive) solution, which could be extended to any degree later. Also if you will like this approach, here is much more comprehensive implementation: angular ui-router login authentication
Firstly, let's have our user service defined like this:
.factory('userService', function ($timeout, $q) {
var user = undefined;
return {
// async way how to load user from Server API
getAuthObject: function () {
var deferred = $q.defer();
// later we can use this quick way -
// - once user is already loaded
if (user) {
return $q.when(user);
}
// server fake call, in action would be $http
$timeout(function () {
// server returned UN authenticated user
user = {isAuthenticated: false };
// here resolved after 500ms
deferred.resolve(user)
}, 500)
return deferred.promise;
},
// sync, quick way how to check IS authenticated...
isAuthenticated: function () {
return user !== undefined
&& user.isAuthenticated;
}
};
})
So, we use async (here $timeout) to load user object form a server. In our example it will have a property {isAuthenticated: false }, which will be used to check if is authenticated.
There is also sync method isAuthenticated() which, until user is loaded and allowed - always returns false.
And that would be our listener of the '$stateChangeStart' event:
.run(['$rootScope', '$state', 'userService',
function ($rootScope, $state, userService) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams
, fromState, fromParams) {
// if already authenticated...
var isAuthenticated = userService.isAuthenticated();
// any public action is allowed
var isPublicAction = angular.isObject(toState.data)
&& toState.data.isPublic === true;
if (isPublicAction || isAuthenticated) {
return;
}
// stop state change
event.preventDefault();
// async load user
userService
.getAuthObject()
.then(function (user) {
var isAuthenticated = user.isAuthenticated === true;
if (isAuthenticated) {
// let's continue, use is allowed
$state.go(toState, toParams)
return;
}
// log on / sign in...
$state.go("login");
})
...
What we are checking first, is if user is already loaded and authenticated (var isAuthenticated = ...). Next we will give green to any public method. This is done with the data {} property of the state object definition (see Attach Custom Data to State Objects)
And that's it. In case of states defined like in a below snippet we can experience:
the 'public', 'home' are allowed to anybody
the 'private', 'private' will redirect to login if isAuthenticated === false
the 'login' in this example provides quick way how to switch isAuthenticated on/off
// States
$stateProvider
// public
.state('home', {
url: "/home",
templateUrl: 'tpl.html',
data: { isPublic: true },
})
.state('public', {
url: "/public",
templateUrl: 'tpl.html',
data: { isPublic: true },
})
// private
.state('private', {
url: "/private",
templateUrl: 'tpl.html',
})
.state('private2', {
url: "/private2",
templateUrl: 'tpl.html',
})
// login
.state('login', {
url: "/login",
templateUrl: 'tpl.html',
data: { isPublic: true },
controller: 'loginCtrl',
})
Check that all here
Some other resources:
angular ui-router login authentication
Angular UI Router: nested states for home to differentiate logged in and logged out
other state data and authentication
Related
I'm working on an AngularJS 1.5.3 project.
There is one page in my app that needs some server information before going to the page. It's kind of like a route guard. No other pages need this information before proceeding.
How can I get this to work in my route resolve? Is this a valid approach to this problem?
e.g.
.state('verifyCredentials', {
url: '/verifyCredentials',
templateUrl: 'verifyCredentials/verify-credentials.html',
controller: 'VerifyCredentialsController',
controllerAs: 'verifyCredentialsController',
resolve: {
//calls http request
authentication
.getInfo()
.then(function(response) {
if(response.goToHome === true) {
//$state.go('home);
} else {
//proceed to verify credentials
}
})
}
})
An AngularJS (1.x) resolve: block is an object on a state definition. Each key is the name of some data to load, and each value is a function which returns a promise for the data.
Resolve functions are injected using Angular’s injector.
An example can be found in the ui-router tutorial.
You are not providing a valid object to your resolve. The authentication should be a value to a key. Here is how your resolve should look.
.state('verifyCredentials', {
url: '/verifyCredentials',
templateUrl: 'verifyCredentials/verify-credentials.html',
controller: 'VerifyCredentialsController',
controllerAs: 'verifyCredentialsController',
resolve: {
authenticate: function(authentication, $state) { // you'll need to inject $state
and your authentication service here if you want to use them.
return authentication
.getInfo()
.then(function(response) {
if(response.goToHome === true) {
$state.go('home');
} else {
//proceed to verify credentials
}
});
}
})
Another possibility is to use the redirectTo method on the state object in order to redirect to a different state depending on the resolve.
Use the redirectTo with the resolve like so:
.state('verifyCredentials', {
resolve: {
authResolve: function(authenticate) {
return authentication.getInfo();
}
},
redirectTo: (trans) => {
// getAsync tells the resolve to load
let resolvePromise = trans.injector().getAsync('authResolve')
return resolvePromise.then(resolveData => {
return resolveData.loggedIn === true ? 'home' : null; // go to home if logged in, else stay on this route in order to continue with credentials flow.
)}
})
Here is how the doc recommend to handle resolves. They use component architecture.
.state('verifyCredentials', {
url: '/verifyCredentials',
component: 'verifyCredentialsComponent',
resolve: {
authenticateData: function(authentication, $state) { // you'll need to inject $state
and your authentication service here if you want to use them.
return authentication
.getInfo()
.then(function(response) {
if(response.goToHome === true) {
$state.go('home');
} else {
//proceed to verify credentials
}
});
}
})
// component
angular.module('app').component('verifyCredentialsComponent', {
bindings: {
authenticateData: '<'
},
template: '<div></div>'
controller: function() {
...
}
})
I've implemented satellizer(https://github.com/sahat/satellizer) as my authentication. What I want is something along (https://github.com/fnakstad/angular-client-side-auth)'s method of access levels and role permission, based on this:
I have added a role field to my userSchema on the server side that. How would I implement a system like this to check which role the authenticated user has, and then use that to show/protect certain routes in app.js's stateprovider?
I've been trying to implement this for some time without any progress.
EDIT:
after checking the link #paul linked this is what I've come to thus far.
.state('admin', {
url: '/admin',
template: 'Scripts/App/partials/admin.html',
controller: 'AdminCtrl',
resolve: {
security: ['$q', function ($q) {
if(!isAdmin()) {
return $q.reject('Not Authorized');
}
}]
}
})
function isAdmin($http, $scope) {
$http.get('http://localhost:3000/api/isadmin')
.success(function (response) {
$scope.role = response.data.role;
console.log($scope.role);
});
if ($scope.role == 'admin') {
console.log($scope.role);
return true;
}
else {
return false;
}
}
App.run(['$rootScope', function ($rootScope) {
$rootScope.$on('$stateChangeError', function (e, toState, toParams, fromState, fromParams, error) {
if (error === "Not Authorized") {
$state.go("notAuthorizedPage");
}
});
}]);
These are all in app.js, and this is where I'm stuck. What am I doing wrong?
In my app i want to resolve a few things before the front page loads. on my other pages its easy for me as i use this function in my resolve:
userAccount.$inject = ['userService','authService'];
function userAccount(userService, authService) {
return authService.firebaseAuthObject.$requireAuth()
.then(function(response) {
return userService.getAccount(response.uid).$loaded();
});
}
And it requires Authorisation, once promise is returned it gets the user data in firebase i saved elsewere. And if the promise is revoked it wont load that page.
But on the home page i want to check if user is logged in, If the user is logged in, grab the users data and goto the page. BUT if the user is not logged in, still go to the page just the user data will be empty.
I currently am using this to check Auth:
checkAuth.$inject = ['userService', 'authService'];
function checkAuth(userService, authService) {
return authService.firebaseAuthObject.$waitForAuth()
}
Which works good, but i still want to resolve the users data before loading the page only IF the user is logged in.
I assumed that this was my answer:
checkAuth.$inject = ['userService','authService'];
function checkAuth(userService, authService) {
return authService.firebaseAuthObject.$waitForAuth()
.then(function(response) {
return checkAuth.getAccount(response.uid).$loaded();
});
}
Which would wait for the promise then load in the users data, but unfortunately this doesnt work.
Any ideas on how i can retrieve the users data once the $waitforauth() function has run, the key is i want to resolve this data before the page loads.
Thanks guys
my state that also doesnt work:
.state('home', {
url: '/',
templateUrl: '/views/main.html',
controller: 'MainCtrl as vm',
resolve: {
checkAuth: checkAuth,
userAccount: function(checkAuth){
console.log(checkAuth);
if(checkAuth){
// grab user data
} else {
return false;
}
}
}
})
This got it done for me.
State:
.state('main', {
url: '/',
templateUrl: '/views/main.html',
controller: 'MainCtrl as vm',
resolve: {
userAccount: userAccountMain
}
})
Function:
userAccountMain.$inject = ['userService', 'authService'];
function userAccountMain(userService, authService) {
return authService.firebaseAuthObject.$waitForAuth()
.then(function(response){
if(response){
return userService.getAccount(response.uid).$loaded();
} else {
return false;
}
});
}
EDIT: forgot to mention that i've been working with AngularJs for a week only, so if you see something you think should be changed for the better and is not related to the question itself feel free to tell me on the comments section.
ok, so I have my authentication Controllers and providers which I won't show because they're irrelevant for the scope of the question.
Then I have an interceptor to check if the user is authenticated when a Call is made. If so I set the Authentication header on the request to include the user's Token if not I redirect the user to the login page and don't even make the request to the server (obviously if someone bypasses this theres also an Authorize on the API).
What I want is to add a few exceptions, meaning there are some pages I want to allow even if the user has no Auth Token. I'm able to this if it's a specific path, but I want to allow my 404 page to be accessed and it's in the Routing that I'm specifying .otherwise to go to the 404 page, how can I make so that my interceptor only redirects to login if it's not going to this page.
The interceptor
.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var authData = localStorageService.get('authorizationData');
var _request = function (config) {
config.headers = config.headers || {};
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
} else if ($location.path != '/accounts/login' && $location.path != '/accounts/register') {
$location.path('/accounts/login');
}
return config;
}
var _responseError = function (rejection) {
if (rejection.status === 401) {
$location.path('/accounts/login');
}
return $q.reject(rejection);
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
return authInterceptorServiceFactory;
}])
and in my Routing
$urlRouterProvider.otherwise('/page-not-found');
$stateProvider
(...)//rest of the states
.state('page-not-found', {
url: '/page-not-found',
templateUrl: '/Content/partials/error/404.html',
data: {
displayName: false
}
})
(...)//rest of the states
I tried to add '/page-not-found' to my if but it won't work as expected because by the time the location is checked for the first time it's still not redirected.
edit
As sugested by charlietfl I'm now trying to use resolve but it's not even passing my function.
I removed this code from my interceptor:
else if ($location.path != '/accounts/login' && $location.path != '/accounts/register') {
$location.path('/accounts/login');
}
and add a new service to the authentication module:
.service('authCheckService', ['$http', '$q', 'localStorageService', function ($http, $q, localStorageService) {
var self = {
'onlyLoggedIn': function ($state, $q) {
var deferred = $q.defer();
var authData = localStorageService.get('authorizationData');
console.log(authData);
if (authData) {
deferred.resolve();
} else {
deferred.reject();
$state.go('login');
}
return deferred.promise;
}
}
return self;
}]);
and i'm trying to call it as:
.state('smo-dashboard', {
url: '/dashboard',
templateUrl: '/Content/partials/dashboard.html',
resolve: authCheckServiceProvider.onlyLoggedIn
})
notice that i'm trying to log authData var to check if it's working but it isn't and there's no error on the console also.
Finally figured out how to solve it using resolve.
first of all I completely removed the interceptor I was using before.
then I made a function inside my Routing .config to use with every resolve for the authentication. finally to handle my resolve I'm using $stateChangeError to redirect to the login state
the Routing Config
.config(function ($stateProvider, $urlRouterProvider) {
// function to check the authentication //
var Auth = ["$q", "authService", function ($q, authService) {
authService.fillAuthData;
if (authService.authentication.isAuth) {
return $q.when(authService.authentication);
} else {
return $q.reject({ authenticated: false });
}
}];
/* if the state does not exist */
$urlRouterProvider
.otherwise('/page-not-found');
$stateProvider
// state that allows non authenticated users //
.state('home', {
url: '/',
templateUrl: '/Content/partials/home.html',
})
// state that needs authentication //
.state('smo-dashboard', {
url: '/dashboard',
templateUrl: '/Content/partials/dashboard.html',
resolve: {
auth: Auth
}
})
// errors //
.state('page-not-found', {
url: '/page-not-found',
templateUrl: '/Content/partials/error/404.html'
})
// accounts //
.state('login', {
url: '/accounts/login',
templateUrl: '/Content/partials/account/login.html'
})
// OTHER STATES //
}
);
in the MainController
$scope.$on("$stateChangeError", function (event, toState, toParams, fromState, fromParams, error) {
$state.go("login");
});
An error service like this could help to handle what to do according to status in responses:
'use strict';
/**
* Error Service
*/
angular.module('app.errorService', [])
.factory("errorService", function ($route, $location) {
return {
checkAndReturnError: function(a,b,c) {
if (a.status === 401){
(function(){
return $location.path("/accounts/login");
}());
return;
}
if (a.status === 404)
return;
alert("Error \n *" + a.data.message);
}
};
});
Then when you do your calls if the response status is 401 it will redirect. The vbad thing agout this is you have to add it to all calls:
$scope.pageChanged = function() {
$scope.Promise = Resource.get({}, function(response) {
}, errorService.checkAndReturnError);
};
I have authentication set up in such a way that I want to prevent any route/state from loading until I know that the user is authorized to access the page. If they are, then the requested page should load, if not, then it should go to the login page.
My config function:
$stateProvider
.state('login', {
url: '/login',
templateUrl : 'login.html',
controller : 'loginController',
data: {
authorizedRoles: [USER_ROLES.guest]
}
})
.state('home', {
url: '/',
templateUrl : 'home.html',
controller : 'homeController',
data: {
authorizedRoles: [USER_ROLES.admin]
}
})
.state('admin', {
url: '/admin',
templateUrl : 'admin.html',
controller : 'adminController',
data: {
authorizedRoles: [USER_ROLES.admin]
}
});
$urlRouterProvider.otherwise('/login');
$locationProvider.html5Mode(true);
My run function:
$rootScope.$on('$stateChangeStart', function(event, next) {
event.preventDefault();
function checkAuthorization() {
if(!AuthService.isAuthorized(authRole)) {
$state.go('login');
} else {
$state.go(next.name);
}
}
if(AuthService.getRoleId() === undefined) {
// We'll have to send request to server to see if user is logged in
AuthService.checkLogin().then(function(response) {
checkAuthorization();
});
} else {
checkAuthorization();
}
})
If I keep the event.preventDefault() in my run function, then the app will be stuck in a loop always going to the requested state. If I remove the event.preventDefault() statement then the app will load the view (which will be visible for a second) before realizing the user should not be allowed to view it (and then go to the correct state).
How can I solve this problem?
You should use resolve and make request to the server to see if user is logged in the resolve
https://github.com/angular-ui/ui-router/wiki#resolve
.state('whatever',{
...
promiseObj: function($http){
// $http returns a promise for the url data
return $http({method: 'GET', url: '/someUrl'}).$promise;
},
...
}
OR
if you have make a call in the controller, make the call in resolve in state, in which your api should response with a 401 if user is not login in and redirect to the log in screen if you have an intercept service.
There is detailed explanation how to do this kind of resolve/wait stuff in this Q & A with working plunker.
An extracted version of the Auth service is:
.factory('userService', function ($timeout, $q) {
var user = undefined;
return {
// async way how to load user from Server API
getAuthObject: function () {
var deferred = $q.defer();
// later we can use this quick way -
// - once user is already loaded
if (user) {
return $q.when(user);
}
// server fake call, in action would be $http
$timeout(function () {
// server returned UN authenticated user
user = {isAuthenticated: false };
// here resolved after 500ms
deferred.resolve(user)
}, 500)
return deferred.promise;
},
// sync, quick way how to check IS authenticated...
isAuthenticated: function () {
return user !== undefined
&& user.isAuthenticated;
}
};
})
where the most important parts are
var user = undefined; - "global" variable which is
containing user and can answer if he has rights
does not contain user (yet) and the answer is "not authorized" then
returned service function: isAuthenticated
That should solve the issue. check more details here