Angular Authentication : Avoid multiple resolve for different routes - javascript

I'm developing an Angular application. In this, I'm authenticating the user before going to dashboard. To achieve this I have wrote the signIn function as
Sign-In Function
this.signIn = function(credentials) {
console.info('AccountController[signIn] Called');
AuthService
.login(credentials)
.then(function(authenticatedUser) {
$scope.globals['currentUser'] = authenticatedUser;
AuthService.setCurrentUser(authenticatedUser);
$scope.globals['isAuthenticated'] = true;
$location.path('/dashboard');
}).catch(function(error) {
console.warn('AccountController[signIn] :: ', error);
Flash.Error(error);
$scope.credentials.password = '';
});
};
I also want to restrict the user from accessing the routes, if they are not logged in. To achieve that I came up with this dirty code.
Routes
$stateProvider
.state('signIn', {
url: '/signIn',
templateUrl: 'partials/signIn/signIn.html',
data: {
pageTitle: 'SignIn'
},
controller: 'AccountController',
controllerAs: 'ac',
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var userInfo = AuthService.isAuthenticated();
console.info('SignIn Route[isAuthenticated] :: ', userInfo);
if (!userInfo) {
return $q.when(userInfo);
} else {
return $q.reject({
isAuthenticated: true
});
}
}]
}
})
.state('dashboard', {
url: '/dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'DashboardController',
access: {
requiredLogin: true
},
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
console.info('dashboard Route[isAuthenticated] :: ', authenticated);
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
})
.state('manageStudent', {
url: '/manageStudent',
templateUrl: 'partials/manageStudent.html',
access: {
requiredLogin: true
},
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
});
App.run(['$rootScope', 'settings', '$state', 'AuthService', '$location', function($rootScope, settings, $state, AuthService, $location) {
$rootScope.$state = $state; // state to be accessed from view
$rootScope.$settings = settings; // state to be accessed from view
$rootScope.$on('$stateChangeStart', function(event, next,nextParams,prev,prevParams) {
// If the user is logged in don't allow him to land on the Login Page
if (next.access !== undefined) {
if (next.access.requiredLogin && !AuthService.isAuthenticated()) {
$location.path('/signIn');
}
}
});
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
if (!error.isAuthenticated) {
console.warn("I'm not Authenticated.Going to Sign-in");
return $location.path('/signIn');
} else {
console.info("I'm Authenticated");
$location.path('/dashboard');
}
});
}]);
Reason I said the above code DIRTY is because, If I have 10 routes which I want to protect from Unauthenticated user, I have to copy the same resolve function in all the routes.
So my question is , what should I do to get rid of multiple resolve function and being able to write DRY code?

Since auth should be resolved on each route change, it is insufficient to just wrap it into separate factory (which is a singleton and will run only once). To get round this limitation it should be a function
app.factory('authResolver', function ($q, AuthService) {
return function () {
// ...
};
});
which runs on every route resolve
...
resolve: {
auth: function (authResolver) {
return authResolver();
}
}
Still not that DRY, but that's the recommended humidity level.
More radical approach that may save the one from boilerplate resolve and save a few lines of code will be similar to that:
app.run(function ($rootScope, authResolver) {
$rootScope.$on('$stateChangeStart', function (e, to) {
if (to.doAuthPlease)
to.resolve.auth = authResolver();
});
});
and
...
doAuthPlease: true,
resolve: {}
The obvious difference with ngRoute in the mentioned answer is that in UI Router you need to have resolve object defined to be able to add new resolvers to the state dynamically. It can be treated like that or leaved as is.

You're on the right track so far. You have what looks like a custom data member access: { requiredLogin: true} on your state objects.
The next step is to use this with the State Change Events that ui-router provides:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
if (toState.access.requiredLogin) {
if(!AuthService.isAuthenticated()) {
event.preventDefault();
// redirect to signIn?
}
}
});
This would be placed in your .run block somewhere which means AuthService needs to be injected there as well. This should remove the need for the resolve block on every route.
Hope that helps.
Update:
if your AuthService.isAuthenticated() function returns a promise, it could be potentially dangerous to rely on the promise to resolve within the event handler (it may move on before the promise resolves). Its probably better that you run the AuthService function before the block (as the application starts) and then store it in a variable:
var isAuth;
AuthService.isAuthenticated().then(function (result) { isAuth = result });
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
if (toState.access.requiredLogin) {
if(!isAuth) {
event.preventDefault();
// redirect to signIn?
}
}
});

var $delegate = $stateProvider.state;
$stateProvider.state = function(name, definition) {
var unrestricted = ['signIn'];
if (unrestricted.indexOf(name) === -1) {
definition.resolve = angular.extend({}, definition.resolve, {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
});
}
return $delegate.apply(this, arguments);
};
Here I'm dynamically adding the resolve to the routes which I want to restrict.

Because you're using ui.router states (and assuming you're using v0.2.0 or greater), you can use state inheritance to solve this with the resolve and not have to duplicate it all over your various states.
What Do Child States Inherit From Parent States?
Child states DO inherit the following from parent states:
Resolved dependencies via resolve
Custom data properties
Nothing else is inherited (no controllers, templates, url, etc).
Inherited Resolved Dependencies
New in version 0.2.0
Child states will inherit resolved dependencies from parent state(s), which they can overwrite. You can then inject resolved dependencies into the controllers and resolve functions of child states.
src - https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#what-do-child-states-inherit-from-parent-states
I accomplish this by using an abstract base state that will defined essentially the same thing you're doing, checking to see if the user is allowed to proceed. Since all of my UI states inherit from the abstract parent state, the authentication dependency is resolved for each of them.
abstract base state
.state('baseState', {
url: '',
abstract: true,
template: '<ui-view></ui-view>'
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
console.info('dashboard Route[isAuthenticated] :: ', authenticated);
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
})
other states
.state('dashboard', {
parent: 'baseState'
url: '/dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'DashboardController',
...
})

Related

Verify login on state change in AngularJS doesn't work well

I want to verify if the user can access a state before he gets there, if he doesn't have permissions will be redirected to another page.
The problem is that I'm doing a SPA and it verifies the permissions, but it takes a while until the server send the response and the user is redirected, so what happen is that a screen appears for 1 or 2 seconds and then is redirected successfully. Is there anyway to avoid this?
This is the code for the state change:
webApp.run(function ($rootScope, $state, StateService) {
$rootScope.$on('$stateChangeStart', function (event, toState, fromState, toParams) {
StateService.hasAccessTo(toState.name, function(data){
if (data.data != ""){
event.preventDefault();
$state.go(data.data);
}
});
});
});
and the service:
webApp.service('StateService', function($http, $rootScope){
this.hasAccessTo = function(state, callback){
$http.get("state/" + state).then(callback);
}
});
I have also tried with a promise in the $stateChangeStart, but it didn't work.
I read about interceptors, but they work if the user is in another page and access mine, if he is already on the page and type a link manually it doesn't intercepts.
Any modifications or suggestions of new ideas or improvements are welcome!
EDIT
Now I have this:
var hasAccessVerification = ['$q', 'StateService', function ($q, $state, StateService) {
var deferred = $q.defer();
StateService.hasAccessTo(this.name, function (data) {
if (data.data !== '') {
$state.go(data.data);
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}];
$urlRouterProvider.otherwise("/");
$compileProvider.debugInfoEnabled(false);
$stateProvider
.state('welcome',{
url:"/",
views: {
'form-view': {
templateUrl: '/partials/form.html',
controller: 'Controller as ctrl'
},
'#': {
templateUrl: '/partials/welcome.html'
}
},
data: {
requireLogin: false
},
resolve: {
hasAccess: hasAccessVerification
}
})
And it validates, but it doesn't load the template. It doesn't show de views. What might I be doing wrong?
EDIT 2
I forgot to add $state here:
var hasAccessVerification = ['$q', '$state', 'StateService', function ($q, $state, StateService){...}
Consider using the resolve in your state configuration instead of using $stateChangeStart event.
According to the docs:
If any of these dependencies are promises, they will be resolved and
converted to a value before the controller is instantiated and the
$stateChangeSuccess event is fired.
Example:
var hasAccessFooFunction = ['$q', 'StateService', function ($q, StateService) {
var deferred = $q.defer();
StateService.hasAccessTo(this.name, function (data) {
if (data.data !== '') {
$state.go(data.data);
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}];
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl: 'views/dashboard.html',
resolve: {
hasAccessFoo: hasAccessFooFunction
}
})
.state('user', {
abstract: true,
url: '/user',
resolve: {
hasAccessFoo: hasAccessFooFunction
},
template: '<ui-view/>'
})
.state('user.create', {
url: '/create',
templateUrl: 'views/user/create.html'
})
.state('user.list', {
url: '/list',
templateUrl: 'views/user/list.html'
})
.state('user.edit', {
url: '/:id',
templateUrl: 'views/user/edit.html'
})
.state('visitors', {
url: '/gram-panchayat',
resolve: {
hasAccessFoo: hasAccessFooFunction
},
templateUrl: 'views/visitor/list.html'
});
And according to the docs https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-resolved-dependencies resolve are inherited:
New in version 0.2.0
Child states will inherit resolved dependencies from parent state(s),
which they can overwrite. You can then inject resolved dependencies
into the controllers and resolve functions of child states.
But, please note:
The resolve keyword MUST be on the state not the views (in case you
use multiple views).
The best practice is to have interceptor on responseError which checks the response status and acts accordingly:
webApp.config(['$httpProvider' ($httpProvider) {
var interceptor = ['$q', '$rootScope', function ($q, $rootScope) {
return {
request: function (config) {
// can also do something here
// for example, add token header
return config;
},
'responseError': function (rejection) {
if (rejection.status == 401 && rejection.config.url !== '/url/to/login') {
// If we're not on the login page
$rootScope.$broadcast('auth:loginRequired');
}
}
return $q.reject(rejection);
}
}
}];
$httpProvider.interceptors.push(interceptor);
}]);
And handle redirection in run block
webApp.run(['$rootScope', function($rootScope){
$rootScope.$on('auth:loginRequired', function () {
$state.go('loginState');
});
}]);
The good thing is that $state service does not need to deal with permission logic:
$stateProvider
.state('someState', {
url: '/some-state',
templateUrl: '/some-state.html',
resolve: {
dataFromBackend: ['dataService', function (postingService) {
// if the request fails, the user gets redirected
return dataService.getData();
}],
},
controller: function ($scope, dataFromBackend) {
}
})
Notice
With this approach, you do not need StateService, all you need to do is to return proper response statuses from backend. For example, if the user is guest, return 401 status.

$state.go in resolve not working

So I have these routes set up:
.state('createOrder', {
url: '/customer-services/orders/create',
templateUrl: 'tpl/customerServices/orders/save.html',
controller: 'SaveOrderController',
controllerAs: 'controller',
resolve: {
order: ['SaveOrderService', function (shared) {
shared.order = { forDelivery: true };
}]
},
data: {
requireLogin: true,
pageTitle: 'Add order'
}
}).state('createOrder.lines', {
url: '/lines',
views: {
'#': {
templateUrl: 'tpl/customerServices/orders/save/line.html',
controller: 'SaveOrderLinesController',
controllerAs: 'controller'
}
},
resolve: {
validate: ['$state', 'SaveOrderService', function ($state, shared) {
// If we don't have an account number
if (!shared.order.accountNumber) {
console.log('redirecting');
// Redirect to the create order view
$state.go('createOrder');
}
}]
},
data: {
requireLogin: true,
pageTitle: 'Add order : Lines'
}
})
But the state does not change. I thought that there might be an error somewhere, so I subscribed the the state events like this:
// On state change
$rootScope.$on('$stateChangeStart', function (event, toState) {
var data = toState.data; // Get our state data
var requireLogin = typeof data === 'undefined' ? false : data.requireLogin; // Check to see if we have any data and if so, check to see if we need login rights
var user = service.get(); // Get our current user
console.log(toState);
$rootScope.currentUser = user; // Set our current user on the rootScope
// If we require login rights and we are not authenticated
if (requireLogin && !user.authenticated) {
event.preventDefault(); // Stop processing
$state.transitionTo('login'); // And redirect to the login page
}
});
$rootScope.$on('$stateNotFound', function () {
console.log('state not found');
});
$rootScope.$on('$stateChangeError', function () {
console.log('state errored');
});
$rootScope.$on('$stateChangeSuccess', function () {
console.log('state changed');
});
and when I refresh my lines view, the console outputs this:
Object { url: "/lines", views: Object, resolve: Object, data: Object, name: "createOrder.lines" } app.js:42:9
redirecting app.js:2436:21
Object { url: "/customer-services", templateUrl: "tpl/customerServices/index.html", controller: "CustomerServicesController", controllerAs: "controller", data: Object, name: "customerServices" } app.js:42:9
state changed app.js:63:9
Object { order: Object, account: null, collectionPoint: null }
As you can see, the states think they have changed, but I still see the createOrder.lines view.
Does anyone have any idea why?
I think you'll need to wrap the $state change in a function that will trigger a digest cycle whilst also rejecting the promise in the resolve method...
$timeout(function() { $state.go("createOrder") });
return $q.reject("Rejection message!");
Remember to inject $timeout and $q into your resolve function! =)
Should also add that rejecting the resolve will fire stateChangeError.
So, it turns out you don't need the promise. Just adding the timeout works.
I found another post which suggests that the timeout is needed to avoid digest issues (which I am guessing is what is causing my states to not change).
Here is the final code:
validate: ['$state', '$timeout', 'SaveOrderService', function ($state, $timeout, shared) {
// If we don't have an account number
if (!shared.order.accountNumber) {
// Timeout to avoid digest issues
$timeout(function () {
// Redirect to the create order view
$state.go('createOrder');
});
}
}]

Trouble passing parameter to child state in ui-router

I have two states, one is a child of the other. One represents a list of people (people) and one for when you click on an individual person to view more details (people.detail).
My first state works as intended, and has several parameters which represent all the various server side filters and paging you could apply. The child state is a modal window, which popups as expected but my only paramater personID never makes it into $stateParams. I wonder if it's something to do the combination of the RESTful style URL and the query string style?
It is perhaps worth noting that $stateParams is populated with everything you'd expect from the parent state.
EDIT: Plunker to show what I mean - http://plnkr.co/edit/eNMIEt?p=info (note that the ID is undefined)
app.js
$urlRouterProvider.otherwise('/people');
$stateProvider
.state('people', {
url: '/people?pageNumber&pageSize&sortField&sortDirection&search&countryID&jobFunctionIDs&firmTypeIDs',
templateUrl: 'Static/js/angular/views/people-list.html',
controller: 'PeopleListController',
resolve: {
api: "api",
people: function (api, $stateParams) {
//Code ommitted
},
countries: function (api) {
//Code ommitted
},
jobFunctions: function (api) {
//Code ommitted
},
firmTypes: function (api) {
//Code ommitted
}
}
});
modalStateProvider.state('people.detail', {
url: "/{personID}",
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function () {
},
resolve: {
person: function (api, $stateParams) {
return api.people.getDetail($stateParams.personID);
}
}
});
The modalStateProvider looks like:
angular.module('myApp')
.provider('modalState', function ($stateProvider) {
var provider = this;
this.$get = function () {
return provider;
}
this.state = function (stateName, options) {
var modalInstance;
$stateProvider.state(stateName, {
url: options.url,
onEnter: function ($modal, $state) {
modalInstance = $modal.open(options);
modalInstance.result['finally'](function () {
modalInstance = null;
if ($state.$current.name === stateName) {
$state.go('^');
}
});
},
onExit: function () {
if (modalInstance) {
modalInstance.close();
}
}
});
};
})
And finally my function in my controller to transition to the people.detail state:
$scope.transitionToPersonDetail = function (personID) {
$state.transitionTo('.detail', { personID: personID }, { location: true, inherit: true, relative: $state.$current, notify: false });
};
After a lot more inspection I'm still not entirely sure why this was happening, I think it had something to do with the modalStateProvider's scope with $stateParams and the fact that the state wasn't "ready". All of this is purely speculation however.
I fixed it with this code:
$stateProvider.state('people.detail', {
url: '/{personID:int}',
onEnter: ['$stateParams', '$state', '$modal', 'api', function($stateParams, $state, $modal, api) {
$modal.open({
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function(person) {
console.log(person);
},
resolve: {
person: function() {
return api.people.getDetail($stateParams.personID);
}
}
}).result['finally'](function(result) {
$state.transitionTo('people');
});
}]
});
As far as I remember the value from 'resolve' is directly available in controller, although I think it's worth checking if controller for your child view is triggered at all
modalStateProvider.state('people.detail', {
url: "/:personID",
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function () {
console.log(person)
},
resolve: {
person: function (api, $stateParams) {
return api.people.getDetail($stateParams.personID);
}
}
});

Angular check authenticate user, before the template is served

When the route change, I try to access to /settings
.when('/settings', {
templateUrl: 'partials/settings',
controller: 'SettingsCtrl',
authenticate: true
})
.run(function ($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function (event, next) {
Auth.checkUser();
if (next.authenticate && !Auth.isLoggedIn()) {
$location.path('/login');
}
});
The code calls to Auth.checkUser() (Asynch) and later to !Auth.isLoggedIn(). The problem is that I need to wait for Auth.checkUser(), that gives me the right status of the user, or find a way to check the response before serving the template.
checkUser: function(user, callback) {
var cb = callback || angular.noop;
return User.check(user,
function(user) {
$rootScope.currentUser = user.id;
return cb(user);
},
function(err) {
return cb(err);
}).$promise;
},
isLoggedIn: function() {
var user = $rootScope.currentUser;
return !!user;
}
You should use the resolve mechanism of angularjs controllers. This way you can define any pre-requisite (service injection, waiting promises, etc) of your controller before render it.
See angular doc : https://docs.angularjs.org/api/ngRoute/provider/$routeProvider
See a working example on angular-app project : https://github.com/angular-app/angular-app/tree/master/client/src/common/security
Example :
.when('/settings', {
templateUrl: 'partials/settings',
controller: 'SettingsCtrl',
authenticate: true,
resolve: {
isUserLogged: ['yourservice' , function(yourservice) {
return yourservice.isUserLoggedPromise();
}]
}
})
Your screen will be rendered only after (and if) isUserLoggedPromise is resolved

AngularJS- Login and Authentication in each route and controller

I have an AngularJS application created by using yeoman, grunt and bower.
I have a login page that has a controller that checks for authentication. If the credentials are correct I reroute to home page.
app.js
'use strict';
//Define Routing for app
angular.module('myApp', []).config(['$routeProvider', '$locationProvider',
function($routeProvider,$locationProvider) {
$routeProvider
.when('/login', {
templateUrl: 'login.html',
controller: 'LoginController'
})
.when('/register', {
templateUrl: 'register.html',
controller: 'RegisterController'
})
.when('/forgotPassword', {
templateUrl: 'forgotpassword.html',
controller: 'forgotController'
})
.when('/home', {
templateUrl: 'views/home.html',
controller: 'homeController'
})
.otherwise({
redirectTo: '/login'
});
// $locationProvider.html5Mode(true); //Remove the '#' from URL.
}]);
angular.module('myApp').factory("page", function($rootScope){
var page={};
var user={};
page.setPage=function(title,bodyClass){
$rootScope.pageTitle = title;
$rootScope.bodylayout=bodyClass;
};
page.setUser=function(user){
$rootScope.user=user;
}
return page;
});
LoginControler.js
'use strict';
angular.module('myApp').controller('LoginController', function($scope, $location, $window,page) {
page.setPage("Login","login-layout");
$scope.user = {};
$scope.loginUser=function()
{
var username=$scope.user.name;
var password=$scope.user.password;
if(username=="admin" && password=="admin123")
{
page.setUser($scope.user);
$location.path( "/home" );
}
else
{
$scope.message="Error";
$scope.messagecolor="alert alert-danger";
}
}
});
On the home page I have
<span class="user-info">
<small>Welcome,</small>
{{user.name}}
</span>
<span class="logout">Logout</span>
In the loginController, I check the login info and if it's successful, I set the user object in the service factory. I don't know whether this is correct or not.
What I need is, When the user is logged in, It sets some value in the user object so that all other pages can get that value.
Whenever any route changes happen, the controller should check if the user is logged in or not. If not, it should reroute to the login page. Also, if the user is already logged in and come back to the page, it should go to home page. The controller should also check the credentials on all of the routes.
I have heard about ng-cookies, but I don't know how to use them.
Many of the examples I saw were not very clear and they use some kind of access roles or something. I don't want that. I only want a login filter.
Can someone give me some ideas?
My solution breaks down in 3 parts: the state of the user is stored in a service, in the run method you watch when the route changes and you check if the user is allowed to access the requested page, in your main controller you watch if the state of the user change.
app.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
$rootScope.$on('$routeChangeStart', function (event) {
if (!Auth.isLoggedIn()) {
console.log('DENY');
event.preventDefault();
$location.path('/login');
}
else {
console.log('ALLOW');
$location.path('/home');
}
});
}]);
You should create a service (I will name it Auth) which will handle the user object and have a method to know if the user is logged or not.
service:
.factory('Auth', function(){
var user;
return{
setUser : function(aUser){
user = aUser;
},
isLoggedIn : function(){
return(user)? user : false;
}
}
})
From your app.run, you should listen the $routeChangeStart event. When the route will change, it will check if the user is logged (the isLoggedIn method should handle it). It won't load the requested route if the user is not logged and it will redirect the user to the right page (in your case login).
The loginController should be used in your login page to handle login. It should just interract with the Auth service and set the user as logged or not.
loginController:
.controller('loginCtrl', [ '$scope', 'Auth', function ($scope, Auth) {
//submit
$scope.login = function () {
// Ask to the server, do your job and THEN set the user
Auth.setUser(user); //Update the state of the user in the app
};
}])
From your main controller, you could listen if the user state change and react with a redirection.
.controller('mainCtrl', ['$scope', 'Auth', '$location', function ($scope, Auth, $location) {
$scope.$watch(Auth.isLoggedIn, function (value, oldValue) {
if(!value && oldValue) {
console.log("Disconnect");
$location.path('/login');
}
if(value) {
console.log("Connect");
//Do something when the user is connected
}
}, true);
Here is another possible solution, using the resolve attribute of the $stateProvider or the $routeProvider. Example with $stateProvider:
.config(["$stateProvider", function ($stateProvider) {
$stateProvider
.state("forbidden", {
/* ... */
})
.state("signIn", {
/* ... */
resolve: {
access: ["Access", function (Access) { return Access.isAnonymous(); }],
}
})
.state("home", {
/* ... */
resolve: {
access: ["Access", function (Access) { return Access.isAuthenticated(); }],
}
})
.state("admin", {
/* ... */
resolve: {
access: ["Access", function (Access) { return Access.hasRole("ROLE_ADMIN"); }],
}
});
}])
Access resolves or rejects a promise depending on the current user rights:
.factory("Access", ["$q", "UserProfile", function ($q, UserProfile) {
var Access = {
OK: 200,
// "we don't know who you are, so we can't say if you're authorized to access
// this resource or not yet, please sign in first"
UNAUTHORIZED: 401,
// "we know who you are, and your profile does not allow you to access this resource"
FORBIDDEN: 403,
hasRole: function (role) {
return UserProfile.then(function (userProfile) {
if (userProfile.$hasRole(role)) {
return Access.OK;
} else if (userProfile.$isAnonymous()) {
return $q.reject(Access.UNAUTHORIZED);
} else {
return $q.reject(Access.FORBIDDEN);
}
});
},
hasAnyRole: function (roles) {
return UserProfile.then(function (userProfile) {
if (userProfile.$hasAnyRole(roles)) {
return Access.OK;
} else if (userProfile.$isAnonymous()) {
return $q.reject(Access.UNAUTHORIZED);
} else {
return $q.reject(Access.FORBIDDEN);
}
});
},
isAnonymous: function () {
return UserProfile.then(function (userProfile) {
if (userProfile.$isAnonymous()) {
return Access.OK;
} else {
return $q.reject(Access.FORBIDDEN);
}
});
},
isAuthenticated: function () {
return UserProfile.then(function (userProfile) {
if (userProfile.$isAuthenticated()) {
return Access.OK;
} else {
return $q.reject(Access.UNAUTHORIZED);
}
});
}
};
return Access;
}])
UserProfile copies the current user properties, and implement the $hasRole, $hasAnyRole, $isAnonymous and $isAuthenticated methods logic (plus a $refresh method, explained later):
.factory("UserProfile", ["Auth", function (Auth) {
var userProfile = {};
var clearUserProfile = function () {
for (var prop in userProfile) {
if (userProfile.hasOwnProperty(prop)) {
delete userProfile[prop];
}
}
};
var fetchUserProfile = function () {
return Auth.getProfile().then(function (response) {
clearUserProfile();
return angular.extend(userProfile, response.data, {
$refresh: fetchUserProfile,
$hasRole: function (role) {
return userProfile.roles.indexOf(role) >= 0;
},
$hasAnyRole: function (roles) {
return !!userProfile.roles.filter(function (role) {
return roles.indexOf(role) >= 0;
}).length;
},
$isAnonymous: function () {
return userProfile.anonymous;
},
$isAuthenticated: function () {
return !userProfile.anonymous;
}
});
});
};
return fetchUserProfile();
}])
Auth is in charge of requesting the server, to know the user profile (linked to an access token attached to the request for example):
.service("Auth", ["$http", function ($http) {
this.getProfile = function () {
return $http.get("api/auth");
};
}])
The server is expected to return such a JSON object when requesting GET api/auth:
{
"name": "John Doe", // plus any other user information
"roles": ["ROLE_ADMIN", "ROLE_USER"], // or any other role (or no role at all, i.e. an empty array)
"anonymous": false // or true
}
Finally, when Access rejects a promise, if using ui.router, the $stateChangeError event will be fired:
.run(["$rootScope", "Access", "$state", "$log", function ($rootScope, Access, $state, $log) {
$rootScope.$on("$stateChangeError", function (event, toState, toParams, fromState, fromParams, error) {
switch (error) {
case Access.UNAUTHORIZED:
$state.go("signIn");
break;
case Access.FORBIDDEN:
$state.go("forbidden");
break;
default:
$log.warn("$stateChangeError event catched");
break;
}
});
}])
If using ngRoute, the $routeChangeError event will be fired:
.run(["$rootScope", "Access", "$location", "$log", function ($rootScope, Access, $location, $log) {
$rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
switch (rejection) {
case Access.UNAUTHORIZED:
$location.path("/signin");
break;
case Access.FORBIDDEN:
$location.path("/forbidden");
break;
default:
$log.warn("$stateChangeError event catched");
break;
}
});
}])
The user profile can also be accessed in the controllers:
.state("home", {
/* ... */
controller: "HomeController",
resolve: {
userProfile: "UserProfile"
}
})
UserProfile then contains the properties returned by the server when requesting GET api/auth:
.controller("HomeController", ["$scope", "userProfile", function ($scope, userProfile) {
$scope.title = "Hello " + userProfile.name; // "Hello John Doe" in the example
}])
UserProfile needs to be refreshed when a user signs in or out, so that Access can handle the routes with the new user profile. You can either reload the whole page, or call UserProfile.$refresh(). Example when signing in:
.service("Auth", ["$http", function ($http) {
/* ... */
this.signIn = function (credentials) {
return $http.post("api/auth", credentials).then(function (response) {
// authentication succeeded, store the response access token somewhere (if any)
});
};
}])
.state("signIn", {
/* ... */
controller: "SignInController",
resolve: {
/* ... */
userProfile: "UserProfile"
}
})
.controller("SignInController", ["$scope", "$state", "Auth", "userProfile", function ($scope, $state, Auth, userProfile) {
$scope.signIn = function () {
Auth.signIn($scope.credentials).then(function () {
// user successfully authenticated, refresh UserProfile
return userProfile.$refresh();
}).then(function () {
// UserProfile is refreshed, redirect user somewhere
$state.go("home");
});
};
}])
The most straightforward manner of defining custom behavior for individual routes would be pretty easy:
1) routes.js: create a new property (like requireAuth) for any desired route
angular.module('yourApp').config(function($routeProvider) {
$routeProvider
.when('/home', {
templateUrl: 'templates/home.html',
requireAuth: true // our custom property
})
.when('/login', {
templateUrl: 'templates/login.html',
})
.otherwise({
redirectTo: '/home'
});
})
2) In a top-tier controller that isn't bound to an element inside the ng-view (to avoid conflict with angular $routeProvider ), check if the newUrl has the requireAuth property and act accordingly
angular.module('YourApp').controller('YourController', function ($scope, $location, session) {
// intercept the route change event
$scope.$on('$routeChangeStart', function (angularEvent, newUrl) {
// check if the custom property exist
if (newUrl.requireAuth && !session.user) {
// user isn’t authenticated
$location.path("/login");
}
});
});
I wrote a post a few months back on how to set up user registration and login functionality with Angular, you can check it out at http://jasonwatmore.com/post/2015/03/10/AngularJS-User-Registration-and-Login-Example.aspx
I check if the user is logged in the $locationChangeStart event, here is my main app.js showing this:
(function () {
    'use strict';
 
    angular
        .module('app', ['ngRoute', 'ngCookies'])
        .config(config)
        .run(run);
 
    config.$inject = ['$routeProvider', '$locationProvider'];
    function config($routeProvider, $locationProvider) {
        $routeProvider
            .when('/', {
                controller: 'HomeController',
                templateUrl: 'home/home.view.html',
                controllerAs: 'vm'
            })
 
            .when('/login', {
                controller: 'LoginController',
                templateUrl: 'login/login.view.html',
                controllerAs: 'vm'
            })
 
            .when('/register', {
                controller: 'RegisterController',
                templateUrl: 'register/register.view.html',
                controllerAs: 'vm'
            })
 
            .otherwise({ redirectTo: '/login' });
    }
 
    run.$inject = ['$rootScope', '$location', '$cookieStore', '$http'];
    function run($rootScope, $location, $cookieStore, $http) {
        // keep user logged in after page refresh
        $rootScope.globals = $cookieStore.get('globals') || {};
        if ($rootScope.globals.currentUser) {
            $http.defaults.headers.common['Authorization'] = 'Basic ' + $rootScope.globals.currentUser.authdata; // jshint ignore:line
        }
 
        $rootScope.$on('$locationChangeStart', function (event, next, current) {
            // redirect to login page if not logged in and trying to access a restricted page
            var restrictedPage = $.inArray($location.path(), ['/login', '/register']) === -1;
            var loggedIn = $rootScope.globals.currentUser;
            if (restrictedPage && !loggedIn) {
                $location.path('/login');
            }
        });
    }
 
})();
I feel like this way is easiest, but perhaps it's just personal preference.
When you specify your login route (and any other anonymous routes; ex: /register, /logout, /refreshToken, etc.), add:
allowAnonymous: true
So, something like this:
$stateProvider.state('login', {
url: '/login',
allowAnonymous: true, //if you move this, don't forget to update
//variable path in the force-page check.
views: {
root: {
templateUrl: "app/auth/login/login.html",
controller: 'LoginCtrl'
}
}
//Any other config
}
You don't ever need to specify "allowAnonymous: false", if not present, it is assumed false, in the check. In an app where most URLs are force authenticated, this is less work. And safer; if you forget to add it to a new URL, the worst that can happen is an anonymous URL is protected. If you do it the other way, specifying "requireAuthentication: true", and you forget to add it to a URL, you are leaking a sensitive page to the public.
Then run this wherever you feel fits your code design best.
//I put it right after the main app module config. I.e. This thing:
angular.module('app', [ /* your dependencies*/ ])
.config(function (/* you injections */) { /* your config */ })
//Make sure there's no ';' ending the previous line. We're chaining. (or just use a variable)
//
//Then force the logon page
.run(function ($rootScope, $state, $location, User /* My custom session obj */) {
$rootScope.$on('$stateChangeStart', function(event, newState) {
if (!User.authenticated && newState.allowAnonymous != true) {
//Don't use: $state.go('login');
//Apparently you can't set the $state while in a $state event.
//It doesn't work properly. So we use the other way.
$location.path("/login");
}
});
});
app.js
'use strict';
// Declare app level module which depends on filters, and services
var app= angular.module('myApp', ['ngRoute','angularUtils.directives.dirPagination','ngLoadingSpinner']);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/login', {templateUrl: 'partials/login.html', controller: 'loginCtrl'});
$routeProvider.when('/home', {templateUrl: 'partials/home.html', controller: 'homeCtrl'});
$routeProvider.when('/salesnew', {templateUrl: 'partials/salesnew.html', controller: 'salesnewCtrl'});
$routeProvider.when('/salesview', {templateUrl: 'partials/salesview.html', controller: 'salesviewCtrl'});
$routeProvider.when('/users', {templateUrl: 'partials/users.html', controller: 'usersCtrl'});
$routeProvider.when('/forgot', {templateUrl: 'partials/forgot.html', controller: 'forgotCtrl'});
$routeProvider.otherwise({redirectTo: '/login'});
}]);
app.run(function($rootScope, $location, loginService){
var routespermission=['/home']; //route that require login
var salesnew=['/salesnew'];
var salesview=['/salesview'];
var users=['/users'];
$rootScope.$on('$routeChangeStart', function(){
if( routespermission.indexOf($location.path()) !=-1
|| salesview.indexOf($location.path()) !=-1
|| salesnew.indexOf($location.path()) !=-1
|| users.indexOf($location.path()) !=-1)
{
var connected=loginService.islogged();
connected.then(function(msg){
if(!msg.data)
{
$location.path('/login');
}
});
}
});
});
loginServices.js
'use strict';
app.factory('loginService',function($http, $location, sessionService){
return{
login:function(data,scope){
var $promise=$http.post('data/user.php',data); //send data to user.php
$promise.then(function(msg){
var uid=msg.data;
if(uid){
scope.msgtxt='Correct information';
sessionService.set('uid',uid);
$location.path('/home');
}
else {
scope.msgtxt='incorrect information';
$location.path('/login');
}
});
},
logout:function(){
sessionService.destroy('uid');
$location.path('/login');
},
islogged:function(){
var $checkSessionServer=$http.post('data/check_session.php');
return $checkSessionServer;
/*
if(sessionService.get('user')) return true;
else return false;
*/
}
}
});
sessionServices.js
'use strict';
app.factory('sessionService', ['$http', function($http){
return{
set:function(key,value){
return sessionStorage.setItem(key,value);
},
get:function(key){
return sessionStorage.getItem(key);
},
destroy:function(key){
$http.post('data/destroy_session.php');
return sessionStorage.removeItem(key);
}
};
}])
loginCtrl.js
'use strict';
app.controller('loginCtrl', ['$scope','loginService', function ($scope,loginService) {
$scope.msgtxt='';
$scope.login=function(data){
loginService.login(data,$scope); //call login service
};
}]);
You can use resolve:
angular.module('app',[])
.config(function($routeProvider)
{
$routeProvider
.when('/', {
templateUrl : 'app/views/login.html',
controller : 'YourController',
controllerAs : 'Your',
resolve: {
factory : checkLoginRedirect
}
})
}
And, the function of the resolve:
function checkLoginRedirect($location){
var user = firebase.auth().currentUser;
if (user) {
// User is signed in.
if ($location.path() == "/"){
$location.path('dash');
}
return true;
}else{
// No user is signed in.
$location.path('/');
return false;
}
}
Firebase also has a method that helps you install an observer, I advise installing it inside a .run:
.run(function(){
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
console.log('User is signed in.');
} else {
console.log('No user is signed in.');
}
});
}
For instance an application has two user called ap and auc. I am passing an extra property to each route and handling the routing based on the data i get in $routeChangeStart.
Try this:
angular.module("app").config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
when('/ap', {
templateUrl: 'template1.html',
controller: 'template1',
isAp: 'ap',
}).
when('/auc', {
templateUrl: 'template2.html',
controller: 'template2',
isAp: 'common',
}).
when('/ic', {
templateUrl: 'template3.html',
controller: 'template3',
isAp: 'auc',
}).
when('/mup', {
templateUrl: 'template4.html',
controller: 'template4',
isAp: 'ap',
}).
when('/mnu', {
templateUrl: 'template5.html',
controller: 'template5',
isAp: 'common',
}).
otherwise({
redirectTo: '/ap',
});
}]);
app.js:
.run(['$rootScope', '$location', function ($rootScope, $location) {
$rootScope.$on("$routeChangeStart", function (event, next, current) {
if (next.$$route.isAp != 'common') {
if ($rootScope.userTypeGlobal == 1) {
if (next.$$route.isAp != 'ap') {
$location.path("/ap");
}
}
else {
if (next.$$route.isAp != 'auc') {
$location.path("/auc");
}
}
}
});
}]);
All have suggested big solution why you are worrying of session on client side.
I mean when state/url changes I suppose you are doing an ajax call to load the data for tempelate.
Note :- To Save user's data you may use `resolve` feature of `ui-router`.
Check cookie if it exist load template , if even cookies doesn't exist than
there is no chance of logged in , simply redirect to login template/page.
Now the ajax data is returned by server using any api. Now the point came into play , return standard return types using server according to logged in status of user. Check those return codes and process your request in controller.
Note:- For controller which doesn't require an ajax call natively , you can call a blank request to server like this server.location/api/checkSession.php and this is checkSession.php
<?php/ANY_LANGUAGE
session_start();//You may use your language specific function if required
if(isset($_SESSION["logged_in"])){
set_header("200 OK");//this is not right syntax , it is just to hint
}
else{
set_header("-1 NOT LOGGED_IN");//you may set any code but compare that same
//code on client side to check if user is logged in or not.
}
//thanks.....
On client side inside controller or through any service as shown in other answers
$http.get(dataUrl)
.success(function (data){
$scope.templateData = data;
})
.error(function (error, status){
$scope.data.error = { message: error, status: status};
console.log($scope.data.error.status);
if(status == CODE_CONFIGURED_ON_SERVER_SIDE_FOR_NON_LOGGED_IN){
//redirect to login
});
Note :- I will update more tomorrow or in future
You should check user authentication in two main sites.
When users change state, checking it using '$routeChangeStart' callback
When a $http request is sent from angular, using an interceptor.

Categories