Route resolve promise, not fully resolved - angularjs - javascript

So, what I'm trying to do here is something simple:
check the role of the loggedUser on each route (with a resolve that sets the user if a token or login credentials are valid on the backend)
redirect to the intended route
if not allowed for a route, redirect to a different route
In my route provider I have something like
$routeProvider
...
.when('/admin', {
templateUrl: 'views/admin/dashboard.html',
controller: 'AdminDashboardCtrl',
resolve: {
checkLoggedUser: check
}
})
...
where ckeck is this function
var check = function($rootScope, $q, AuthService) {
var deferred = $q.defer();
if($rootScope.loggedUser) {
return;
}
console.log('inside resolve check')
AuthService.check().success(function(data) {
$rootScope.loggedUser = data.user;
deferred.resolve(data.user);
});
console.log('finished check')
return deferred.promise;
};
And my AuthService.check() is this function
check: function()
{
var authtoken = StorageService.get('authtoken');
if(!authtoken) {
$location.path('login');
}
console.log('before returning');
return $http.post($rootScope.base + 'auth/authenticate', { 'authtoken': authtoken });
},
In my .run(function I have
$rootScope.$on('$routeChangeSuccess', function() {
setIntendedUrl();
console.log($rootScope.loggedUser);
console.log($location.path());
});
and setIntendedUrl() check for the loggedUser and redirects to the correct page (or, in what I'm trying to accomplish, redirect to a different page if not allowed, for example the loggedUser has role = 1, can visit only the routes /admin, if a user has role = 2, and the requested path is /admin, he has to be redirected to /user)
So after all this code, when the app run this is my log in the console (see in the code where are they called)
inside resolve check app.js:29
before returning authservice.js:24
finished check app.js:36
intended: /admin/agents/create app.js:149 <--- here is where I redirect the user
Object {id: "21", name: "Kyle", surname: "Butler", roleId: "2"...} app.js:167
/admin/agents/create <--- requested path
This is not what I was expecting, so the first three logs are good, the third doesn't wait the promise to be returned (so I don't have a loggedUser) then the AuthService:check() returns the user and it's everything done at this point, the user with role = 2 is in a route that is not allowed to see.
Just to complete the code, this is the setIntendedUrl function
var setIntendedUrl = function() {
intended = $location.path();
console.log('intended: ' + intended)
if(intended !== '/login') {
if($rootScope.loggedUser && $rootScope.loggedUser.roleId === '1' && !/^\/admin*/.test(intended)) {
intended = '/admin';
} else if($rootScope.loggedUser && $rootScope.loggedUser.roleId === '2' && !/^\/manager*/.test(intended)) {
intended = '/manager';
}
StorageService.set('intended', intended);
//$location.path(intended);
}
};
What I am doing wrong? Why the user in the check function is not resolved before the other code is executed?

Can you make use of session/locals storage or $rootScope where you can store the users authorization object with given routes, permission info once user logged in.
Now is route resolve or run() block you can retrieve the user auth object perform authorization action.
e.g.
.run(['sessionService', '$rootScope', '$location', function(sessionService, $rootScope, $location) {
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
var currentUser = sessionService.get('user_details');
if(next.taskAccess && next.taskAccess != ""){
var hasPerm = $rootScope.getPermission(next.taskAccess);
if(!hasPerm){
$location.path('/unauthorized');
}
}
});
}]);

Related

Angular UI Router 1.0.0 - prevent route load with $transitions.onBefore

I upgraded to UI Router 1.0.0, which has changed from .$on($stateChangeX to $transitions.onX( (see $transitions here).
I need to resolve the user's profile and their access BEFORE navigation to the page (e.g. the user should never see the page they're attempting to transition to). Before, I was able to use a resolve directly in the state and pass thru things I need sequentially, as such:
state('my-state', {
...
resolve : {
ProfileLoaded : ['$rootScope', function ($rootScope) {
return $rootScope.loadProfile();
}],
access: ['Access', 'ProfileLoaded', function (Access, ProfileLoaded) {
return Access.hasORRoles(['admin']); //either $q.reject(status code); or "200"
}]
}
})
Then I could easily retrieve the error type in $stateChangeError:
app.run(...
$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
if (error === 401) {
$state.go('home');
}
...
With $transitions, I'm trying to do the same thing...
.state('user.my-page', {
...
data: {
loadProfile: true,
roles: [
'admin'
]
}
});
app.run(...
$transitions.onBefore({to: profile}, function(trans) {
return loadProfile().then(function (prof) {
var substate = trans.to();
return Access.hasORRoles(substate.data.roles); //either $q.reject(status code); or "200"
});
});
$transitions.onError({}, function(trans) {
var error = trans && trans._error;
if (error == 401) {
$state.go('home');
}
...
So my question is:
Does onBefore do the same thing as resolve in terms of ensuring the user can't navigate before data is checked? The page is still loading and THEN redirecting with $state.go after the page loads.
Just for people who are still landing here.
You can return a promisse to prevent the site from loading.
$transitions.onBefore({}, function(transition) {
return new Promise((resolve) => {
// Do auth..
return Access.hasORRoles(['admin']);
});
});

How to use AngularJS to store a 'Token' for the lifetime of the app?

I'm using AngularJS to login a user (using Restangular). The login controller gets returned a 'Token' which I then need to use on every request thereafter.
My question is, what is the best way to store this Token using AngularJS? It needs to exist for the lifetime of the app.
I was looking at services but I have to keep injecting it into the controllers on to keep it alive.
lifetime is not promise as far as you are using web apps, but if you want you can use localstorage,
here is an example service how to use localstorage in angular, you can add it to service.js file:
var storeService = innovidServices.factory('storeService', function() {
var service =
{
setClientData:function(client_details)
{
window.localStorage.setItem( "client_data", JSON.stringify(client_details) );
client_data = client_details;
},
getClientData:function()
{
if (client_data == null)
{
client_data = JSON.parse(window.localStorage.getItem("client_data"));
}
return client_data;
}
}
var client_data = null;
return service;
});
I think the best way is store this 'Token' in the $rootScope.
myapp.controller('loginCtrl', function($scope, $rootScope) {
...
$rootScope.token = Token;
...
});
Then use http interceptor to inject this as for example GET parameter to every query
myapp.factory('httpTokenInterceptor', function ($rootScope) {
return {
request: function (config) {
var token = $rootScope.token;
if (token) {
config.url = URI(config.url).addSearch({'token':token}).toString();
}
return config;
}
};
});
myapp.config(function ($httpProvider) {
$httpProvider.interceptors.push('httpTokenInterceptor');
});

How to turn off eventlistener in factory method which is returning promise to controllers?

I have a loginService factory used to perform login, logout and provide user data to controllers. Because I need to update userdata in controllers every time loginstate changes, my factory method is returning an update promise:
app.controller('TestCtrl', function ($scope, loginService) {
loginService.currentUserData().then(null, null, function(CurrUserData){
$scope.CurrUserData = CurrUserData;
});
});
In loginService I'm listening to $firebaseSimpleLogin:login/logout events and after they're fired, I pass the userdata object (returned by function based on UID) or null ($fbSimpleLogin:logout event) to $emit.
And finally, in my loginService.currentUserData() method I'm listening to this emitted events and returning deferred.notify(userdata/null).
First issue is that when I change the view (template+ctrl+location), I need to invoke $firebaseSimpleLogin:login/logout event to deliver my userData to new controller. Now, I'm doing it by $locationChangeStart event, but there should be better way...
And last issue: when I'm changing the view, there are more data calls, than I expectet.
Probably every controller add event listeners on $rootScope by calling loginService.currentUserData()? Described code below:
$rootScope.$on('$firebaseSimpleLogin:login', function (e, authUser) {
findUserByUid(authUser.uid);
});
$rootScope.$on('$firebaseSimpleLogin:logout', function() {
$rootScope.$emit('userLogout', null);
});
$rootScope.$on('$locationChangeStart', function(event, next, current) {
currentUser().then(function(u){
$timeout(function() { // without this same event on viewchange is fired
// by simplelogin, collision (I need to replace this whole block with invoking simpleloginevent)
if (u) {$rootScope.$emit('$firebaseSimpleLogin:login', u);
} else {$rootScope.$emit('$firebaseSimpleLogin:logout', null);};
}, 150);
});
});
function findUserByUid (uid) {
var query = $firebase(usersRef.startAt(uid).endAt(uid));
query.$on('loaded', function () {
var username = query.$getIndex()[0];
setCurrentUser(username);
});
}
function setCurrentUser (username) {
if (username) {$rootScope.$emit('userData', $firebase(usersRef).$child(username));};
}
var currentUserData = function () { // this method is used in CTRLs
var deferred = $q.defer();
var uDl = $rootScope.$on('userData', function(e, FbUserData){deferred.notify(FbUserData); });
var uLl = $rootScope.$on('userLogout', function(){deferred.notify(null); });
return deferred.promise;
};
I recently wrote a demo AngularFire app that has similar functionality. The way I found to handle this is only worry about three points.
When the user logs in $rootScope.$on('$firebaseSimpleLogin:$login')
When the user logs out $rootScope.$on('$firebaseSimpleLogin:$logout')
Calling $getCurrentUser()
This will be able to capture the login life cycle. Since you need to know who the current user is, you can rely on the $firebaseSimpleLogin method rather than trying to $emit your own events.
You also could resolve the current user in the $routeProvider for each view. This way each view won't be rendered until the user has been loaded.
Here's the plunker project and the example Factory:
http://plnkr.co/edit/M0UJmm?p=preview
// Auth factory that encapsulates $firebaseSimpleLogin methods
// provides easy use of capturing events that were emitted
// on the $rootScope when users login and out
.factory('Auth', function($firebaseSimpleLogin, Fb, $rootScope) {
var simpleLogin = $firebaseSimpleLogin(Fb);
return {
getCurrentUser: function() {
return simpleLogin.$getCurrentUser();
},
login: function(provider, user) {
simpleLogin.$login(provider, {
email: user.email,
password: user.password
});
},
logout: function() {
simpleLogin.$logout();
},
onLogin: function(cb) {
$rootScope.$on('$firebaseSimpleLogin:login',
function(e, user) {
cb(e, user);
});
},
onLogout: function(cb) {
$rootScope.$on('$firebaseSimpleLogin:logout',
function(e, user) {
cb(e, user);
});
}
}
})

firebase simple login tutorial missing user

Tutorial: http://www.thinkster.io/angularjs/wBhtRLWHIR/6-authenticating-users-with-a-service
I'm following this tutorial and it seems like I'm losing my user as soon as they register.
Here is my auth.js factory:
'use strict';
app.factory('Auth', function($firebaseSimpleLogin, FIREBASE_URL, $rootScope){
var ref = new Firebase(FIREBASE_URL);
var auth = $firebaseSimpleLogin(ref);
var Auth = {
register : function(user) {
return auth.$createUser(user.email, user.password);
},
signedIn : function() {
// PROBLEM: authUser is always null
console.log(auth.user);
return auth.user !== null;
},
logout : function () {
auth.$logout();
}
};
$rootScope.signedIn = function () {
return Auth.signedIn();
};
return Auth;
});
Here is my auth.js controller:
'use strict';
app.controller('AuthCtrl', function($scope, $location, Auth){
if (Auth.signedIn()) {
$location.path('/');
}
$scope.register = function () {
Auth.register($scope.user).then(function (authUser) {
console.log(authUser);
$location.path('/');
});
};
});
The console.log under signedIn in the factory is always null. Any idea where the disconnect is? The registration itself is working fine, and authUser is populated in the console.log in the controller when registering.
The latest documentation for Angularfire says that the $createUser method of $firebaseSimpleLogin returns a promise but it doesn't mention any parameters being passed to the then callback.
You can use the $getCurrentUser method to get the current user after the user registers.
The tutorial needs to be updated and you should always be checking the documentation for whatever libraries you're using yourself.
Your code for signedIn should look like this:
Auth.signedIn = function() {
auth.$getCurrentUser().then(function(currentUser) {
console.log(currentUser);
}, function() {
console.log('error');
});
};
I found a very similar question that is further along in the tutorial :can't show logout button after $createUser
In the answer I learned that angularfire used to automatically log the user in after it was created. Apparently now it no longer does that which is why the auth.user in signedIn was null.
I am now doing the same tutorial. This code (in auth controller) worked for me:
$scope.register = function () {
Auth.register($scope.user).then(function (authUser) {
console.log(authUser);
Auth.login($scope.user);
$location.path('/');
});
};
I'm a total n00b, but what (I think) this is doing is authenticating the user, then running a function that logs the user in right after. Logout button is now functioning as expected.

AngularJS Authentication + RESTful API

Angular+RESTful Client-side Communication w/ API for Auth/(re)Routing
This has been covered in a few different questions, and in a few different tutorials, but all of the previous resources I've encountered don't quite hit the nail on the head.
In a nut-shell, I need to
Login via POST from http://client.foo to http://api.foo/login
Have a "logged in" GUI/component state for the user that provides a logout route
Be able to "update" the UI when the user logs out / logs out.
This has been the most frustrating
Secure my routes to check for authenticated-state (should they need it) and redirect the user to the login page accordingly
My issues are
Every time I navigate to a different page, I need to make the call to api.foo/status to determine whether or not user is logged in. (ATM I'm using Express for routes) This causes a hiccup as Angular determines things like ng-show="user.is_authenticated"
When I successfully login/logout, I need to refresh the page (I don't want to have to do this) in order to populate things like {{user.first_name}}, or in the case of logging out, empty that value out.
// Sample response from `/status` if successful
{
customer: {...},
is_authenticated: true,
authentication_timeout: 1376959033,
...
}
What I've tried
http://witoldsz.github.io/angular-http-auth/1
http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/2
https://github.com/mgonto/restangular (For the life of me I could not figure out how to POST with post data and not query params. The docs turned up nothing on the matter.
Why I feel like I'm losing my mind
It seems as though every tutorial relies on some database (lots of Mongo, Couch, PHP+MySQL, ad infinitum) solution, and none rely purely on communication with a RESTful API to persist logged-in states. Once logged in, additional POSTs/GETs are sent with withCredentials:true, so that's not the issue
I cannot find ANY examples/tutorials/repos that do Angular+REST+Auth, sans a backend language.
I'm not too proud
Admittedly, I'm new to Angular, and would not be surprised if I'm approaching this in a ridiculous way; I'd be thrilled if someone suggest an alternative—even if it's soup-to-nuts.
I'm using Express mostly because I really love Jade and Stylus— I'm not married to the Express' routing and will give it up if what I want to do is only possible with Angular's routing.
Thanks in advance for any help anyone can provide. And please don't ask me to Google it, because I have about 26 pages of purple links. ;-)
1This solution relies on Angular's $httpBackend mock, and it's unclear how to make it talk to a real server.
2This was the closest, but since I have an existing API I need to authenticate with, I could not use passport's 'localStrategy', and it seemed insane to write an OAUTH service...that only I intended to use.
This is taken from my blog post on url route authorisation and element security here but I will briefly summaries the main points :-)
Security in frontend web application is merely a starting measure to stop Joe Public, however any user with some web knowledge can circumvent it so you should always have security server-side as well.
The main concern around security stuff in angular is route security, luckily when defining a route in angular you are create an object, an object that can have other properties. The cornerstone to my approach is to add a security object to this route object which basically defines the roles the user must be in to be able to access a particular route.
// route which requires the user to be logged in and have the 'Admin' or 'UserManager' permission
$routeProvider.when('/admin/users', {
controller: 'userListCtrl',
templateUrl: 'js/modules/admin/html/users.tmpl.html',
access: {
requiresLogin: true,
requiredPermissions: ['Admin', 'UserManager'],
permissionType: 'AtLeastOne'
});
The whole approach focuses around an authorisation service which basically does the check to see if the user has the required permissions. This service abstract the concerns away from the other parts of this solution to do with the user and their actual permission that would have been retrieved from the server during login. While the code is quite verbose it is fully explained in my blog post. However, it basically handle the permission check and two modes of authorisation. The first is that the user must have at least on of the defined permissions, the second is the user must have all of the defined permissions.
angular.module(jcs.modules.auth.name).factory(jcs.modules.auth.services.authorization, [
'authentication',
function (authentication) {
var authorize = function (loginRequired, requiredPermissions, permissionCheckType) {
var result = jcs.modules.auth.enums.authorised.authorised,
user = authentication.getCurrentLoginUser(),
loweredPermissions = [],
hasPermission = true,
permission, i;
permissionCheckType = permissionCheckType || jcs.modules.auth.enums.permissionCheckType.atLeastOne;
if (loginRequired === true && user === undefined) {
result = jcs.modules.auth.enums.authorised.loginRequired;
} else if ((loginRequired === true && user !== undefined) &&
(requiredPermissions === undefined || requiredPermissions.length === 0)) {
// Login is required but no specific permissions are specified.
result = jcs.modules.auth.enums.authorised.authorised;
} else if (requiredPermissions) {
loweredPermissions = [];
angular.forEach(user.permissions, function (permission) {
loweredPermissions.push(permission.toLowerCase());
});
for (i = 0; i < requiredPermissions.length; i += 1) {
permission = requiredPermissions[i].toLowerCase();
if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.combinationRequired) {
hasPermission = hasPermission && loweredPermissions.indexOf(permission) > -1;
// if all the permissions are required and hasPermission is false there is no point carrying on
if (hasPermission === false) {
break;
}
} else if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.atLeastOne) {
hasPermission = loweredPermissions.indexOf(permission) > -1;
// if we only need one of the permissions and we have it there is no point carrying on
if (hasPermission) {
break;
}
}
}
result = hasPermission ?
jcs.modules.auth.enums.authorised.authorised :
jcs.modules.auth.enums.authorised.notAuthorised;
}
return result;
};
Now that a route has security you need a way of determining if a user can access the route when a route change has been started. To do this we be intercepting the route change request, examining the route object (with our new access object on it) and if the user cannot access the view we replace the route with another one.
angular.module(jcs.modules.auth.name).run([
'$rootScope',
'$location',
jcs.modules.auth.services.authorization,
function ($rootScope, $location, authorization) {
$rootScope.$on('$routeChangeStart', function (event, next) {
var authorised;
if (next.access !== undefined) {
authorised = authorization.authorize(next.access.loginRequired,
next.access.permissions,
next.access.permissionCheckType);
if (authorised === jcs.modules.auth.enums.authorised.loginRequired) {
$location.path(jcs.modules.auth.routes.login);
} else if (authorised === jcs.modules.auth.enums.authorised.notAuthorised) {
$location.path(jcs.modules.auth.routes.notAuthorised).replace();
}
}
});
}]);
The key here really is the '.replace()' as this replace the current route (the one they have not got rights to see) with the route we are redirecting them to. This stop any then navigating back to the unauthorised route.
Now we can intercept routes we can do quite a few cool things including redirecting after a login if a user landed on a route that they needed to be logged in for.
The second part of the solution is being able to hide/show UI element to the user depending on there rights. This is achieve via a simple directive.
angular.module(jcs.modules.auth.name).directive('access', [
jcs.modules.auth.services.authorization,
function (authorization) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var makeVisible = function () {
element.removeClass('hidden');
},
makeHidden = function () {
element.addClass('hidden');
},
determineVisibility = function (resetFirst) {
var result;
if (resetFirst) {
makeVisible();
}
result = authorization.authorize(true, roles, attrs.accessPermissionType);
if (result === jcs.modules.auth.enums.authorised.authorised) {
makeVisible();
} else {
makeHidden();
}
},
roles = attrs.access.split(',');
if (roles.length > 0) {
determineVisibility(true);
}
}
};
}]);
You would then sure an element like so:
<button type="button" access="CanEditUser, Admin" access-permission-type="AtLeastOne">Save User</button>
Read my full blog post for a much more detailed overview to the approach.
I haven't been using $resource because I'm just hand crafting my service calls for my application. That said I've handled login by having a service which depends on all the other services that get some sort of initialization data. When the login succeeds it triggers for initialization of all the services.
Within my controller scope I watch the loginServiceInformation and populate some properties of the model accordingly (to trigger the appropriate ng-show/hide). With regard to routing I'm using Angular's built in routing and I simply have an ng-hide based on the loggedIn boolean shown here, it shows text to request login or else the div with the ng-view attribute (so if not logged in immediately after login you're on the correct page, currently I load data for all views but I believe this could be more selective if necessary)
//Services
angular.module("loginModule.services", ["gardenModule.services",
"surveyModule.services",
"userModule.services",
"cropModule.services"
]).service(
'loginService',
[ "$http",
"$q",
"gardenService",
"surveyService",
"userService",
"cropService",
function ( $http,
$q,
gardenService,
surveyService,
userService,
cropService) {
var service = {
loginInformation: {loggedIn:false, username: undefined, loginAttemptFailed:false, loggedInUser: {}, loadingData:false},
getLoggedInUser:function(username, password)
{
service.loginInformation.loadingData = true;
var deferred = $q.defer();
$http.get("php/login/getLoggedInUser.php").success(function(data){
service.loginInformation.loggedIn = true;
service.loginInformation.loginAttemptFailed = false;
service.loginInformation.loggedInUser = data;
gardenService.initialize();
surveyService.initialize();
userService.initialize();
cropService.initialize();
service.loginInformation.loadingData = false;
deferred.resolve(data);
}).error(function(error) {
service.loginInformation.loggedIn = false;
deferred.reject(error);
});
return deferred.promise;
},
login:function(username, password)
{
var deferred = $q.defer();
$http.post("php/login/login.php", {username:username, password:password}).success(function(data){
service.loginInformation.loggedInUser = data;
service.loginInformation.loggedIn = true;
service.loginInformation.loginAttemptFailed = false;
gardenService.initialize();
surveyService.initialize();
userService.initialize();
cropService.initialize();
deferred.resolve(data);
}).error(function(error) {
service.loginInformation.loggedInUser = {};
service.loginInformation.loggedIn = false;
service.loginInformation.loginAttemptFailed = true;
deferred.reject(error);
});
return deferred.promise;
},
logout:function()
{
var deferred = $q.defer();
$http.post("php/login/logout.php").then(function(data){
service.loginInformation.loggedInUser = {};
service.loginInformation.loggedIn = false;
deferred.resolve(data);
}, function(error) {
service.loginInformation.loggedInUser = {};
service.loginInformation.loggedIn = false;
deferred.reject(error);
});
return deferred.promise;
}
};
service.getLoggedInUser();
return service;
}]);
//Controllers
angular.module("loginModule.controllers", ['loginModule.services']).controller("LoginCtrl", ["$scope", "$location", "loginService", function($scope, $location, loginService){
$scope.loginModel = {
loadingData:true,
inputUsername: undefined,
inputPassword: undefined,
curLoginUrl:"partials/login/default.html",
loginFailed:false,
loginServiceInformation:{}
};
$scope.login = function(username, password) {
loginService.login(username,password).then(function(data){
$scope.loginModel.curLoginUrl = "partials/login/logoutButton.html";
});
}
$scope.logout = function(username, password) {
loginService.logout().then(function(data){
$scope.loginModel.curLoginUrl = "partials/login/default.html";
$scope.loginModel.inputPassword = undefined;
$scope.loginModel.inputUsername = undefined;
$location.path("home");
});
}
$scope.switchUser = function(username, password) {
loginService.logout().then(function(data){
$scope.loginModel.curLoginUrl = "partials/login/loginForm.html";
$scope.loginModel.inputPassword = undefined;
$scope.loginModel.inputUsername = undefined;
});
}
$scope.showLoginForm = function() {
$scope.loginModel.curLoginUrl = "partials/login/loginForm.html";
}
$scope.hideLoginForm = function() {
$scope.loginModel.curLoginUrl = "partials/login/default.html";
}
$scope.$watch(function(){return loginService.loginInformation}, function(newVal) {
$scope.loginModel.loginServiceInformation = newVal;
if(newVal.loggedIn)
{
$scope.loginModel.curLoginUrl = "partials/login/logoutButton.html";
}
}, true);
}]);
angular.module("loginModule", ["loginModule.services", "loginModule.controllers"]);
The HTML
<div style="height:40px;z-index:200;position:relative">
<div class="well">
<form
ng-submit="login(loginModel.inputUsername, loginModel.inputPassword)">
<input
type="text"
ng-model="loginModel.inputUsername"
placeholder="Username"/><br/>
<input
type="password"
ng-model="loginModel.inputPassword"
placeholder="Password"/><br/>
<button
class="btn btn-primary">Submit</button>
<button
class="btn"
ng-click="hideLoginForm()">Cancel</button>
</form>
<div
ng-show="loginModel.loginServiceInformation.loginAttemptFailed">
Login attempt failed
</div>
</div>
</div>
The Base HTML that uses the parts above to complete the picture:
<body ng-controller="NavigationCtrl" ng-init="initialize()">
<div id="outerContainer" ng-controller="LoginCtrl">
<div style="height:20px"></div>
<ng-include src="'partials/header.html'"></ng-include>
<div id="contentRegion">
<div ng-hide="loginModel.loginServiceInformation.loggedIn">Please login to continue.
<br/><br/>
This new version of this site is currently under construction.
<br/><br/>
If you need the legacy site and database click here.</div>
<div ng-view ng-show="loginModel.loginServiceInformation.loggedIn"></div>
</div>
<div class="clear"></div>
<ng-include src="'partials/footer.html'"></ng-include>
</div>
</body>
I have the login controller defined with an ng-controller higher up in the DOM so that I can change the body area of my page based on the loggedIn variable.
Note I haven't implemented form validation here yet. Also admittedly still quite fresh to Angular so any pointers to things in this post are welcome. Although this doesn't answer the question directly since it isn't a RESTful based implementation I believe the same can be adapted to $resources since it's built on top of $http calls.
I've written an AngularJS module for UserApp that does pretty much everything you ask for. You could either:
Modify the module and attach the functions to your own API, or
Use the module together with the user management API, UserApp
https://github.com/userapp-io/userapp-angular
It supports protected/public routes, rerouting on login/logout, heartbeats for status checks, stores the session token in a cookie, events, etc.
If you want to give UserApp a try, take the course on Codecademy.
Here's some examples of how it works:
Login form with error handling:
<form ua-login ua-error="error-msg">
<input name="login" placeholder="Username"><br>
<input name="password" placeholder="Password" type="password"><br>
<button type="submit">Log in</button>
<p id="error-msg"></p>
</form>
Signup form with error handling:
<form ua-signup ua-error="error-msg">
<input name="first_name" placeholder="Your name"><br>
<input name="login" ua-is-email placeholder="Email"><br>
<input name="password" placeholder="Password" type="password"><br>
<button type="submit">Create account</button>
<p id="error-msg"></p>
</form>
How to specify which routes that should be public, and which route that is the login form:
$routeProvider.when('/login', {templateUrl: 'partials/login.html', public: true, login: true});
$routeProvider.when('/signup', {templateUrl: 'partials/signup.html', public: true});
The .otherwise() route should be set to where you want your users to be redirected after login. Example:
$routeProvider.otherwise({redirectTo: '/home'});
Log out link:
<a href="#" ua-logout>Log Out</a>
(Ends the session and redirects to the login route)
Access user properties:
User info is accessed using the user service, e.g: user.current.email
Or in the template: <span>{{ user.email }}</span>
Hide elements that should only be visible when logged in:
<div ng-show="user.authorized">Welcome {{ user.first_name }}!</div>
Show an element based on permissions:
<div ua-has-permission="admin">You are an admin</div>
And to authenticate to your back-end services, just use user.token() to get the session token and send it with the AJAX request. At the back-end, use the UserApp API (if you use UserApp) to check if the token is valid or not.
If you need any help, just let me know :)
I've created a github repo summing up this article basically: https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec
ng-login Github repo
Plunker
I'll try to explain as good as possible, hope I help some of you out there:
(1) app.js: Creation of authentication constants on app definition
var loginApp = angular.module('loginApp', ['ui.router', 'ui.bootstrap'])
/*Constants regarding user login defined here*/
.constant('USER_ROLES', {
all : '*',
admin : 'admin',
editor : 'editor',
guest : 'guest'
}).constant('AUTH_EVENTS', {
loginSuccess : 'auth-login-success',
loginFailed : 'auth-login-failed',
logoutSuccess : 'auth-logout-success',
sessionTimeout : 'auth-session-timeout',
notAuthenticated : 'auth-not-authenticated',
notAuthorized : 'auth-not-authorized'
})
(2) Auth Service: All following functions are implemented in auth.js service. The $http service is used to communicate with the server for the authentication procedures. Also contains functions on authorization, that is if the user is allowed to perform a certain action.
angular.module('loginApp')
.factory('Auth', [ '$http', '$rootScope', '$window', 'Session', 'AUTH_EVENTS',
function($http, $rootScope, $window, Session, AUTH_EVENTS) {
authService.login() = [...]
authService.isAuthenticated() = [...]
authService.isAuthorized() = [...]
authService.logout() = [...]
return authService;
} ]);
(3) Session: A singleton to keep user data. The implementation here depends on you.
angular.module('loginApp').service('Session', function($rootScope, USER_ROLES) {
this.create = function(user) {
this.user = user;
this.userRole = user.userRole;
};
this.destroy = function() {
this.user = null;
this.userRole = null;
};
return this;
});
(4) Parent controller: Consider this as the "main" function of your application, all controllers inherit from this controller, and it's the backbone of the authentication of this app.
<body ng-controller="ParentController">
[...]
</body>
(5) Access control: To deny access on certain routes 2 steps have to be implemented:
a) Add data of the roles allowed to access each route, on ui router's $stateProvider service as can be seen below (same can work for ngRoute).
.config(function ($stateProvider, USER_ROLES) {
$stateProvider.state('dashboard', {
url: '/dashboard',
templateUrl: 'dashboard/index.html',
data: {
authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
}
});
})
b) On $rootScope.$on('$stateChangeStart') add the function to prevent state change if the user is not authorized.
$rootScope.$on('$stateChangeStart', function (event, next) {
var authorizedRoles = next.data.authorizedRoles;
if (!Auth.isAuthorized(authorizedRoles)) {
event.preventDefault();
if (Auth.isAuthenticated()) {
// user is not allowed
$rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
} else {d
// user is not logged in
$rootScope.$broadcast(AUTH_EVENTS.notAuthenticated);
}
}
});
(6) Auth interceptor: This is implemented, but can't be checked on the scope of this code. After each $http request, this interceptor checks the status code, if one of the below is returned, then it broadcasts an event to force the user to log-in again.
angular.module('loginApp')
.factory('AuthInterceptor', [ '$rootScope', '$q', 'Session', 'AUTH_EVENTS',
function($rootScope, $q, Session, AUTH_EVENTS) {
return {
responseError : function(response) {
$rootScope.$broadcast({
401 : AUTH_EVENTS.notAuthenticated,
403 : AUTH_EVENTS.notAuthorized,
419 : AUTH_EVENTS.sessionTimeout,
440 : AUTH_EVENTS.sessionTimeout
}[response.status], response);
return $q.reject(response);
}
};
} ]);
P.S. A bug with the form data autofill as stated on the 1st article can be easily avoided by adding the directive that is included in directives.js.
P.S.2 This code can be easily tweaked by the user, to allow different routes to be seen, or display content that was not meant to be displayed. The logic MUST be implemented server-side, this is just a way to show things properly on your ng-app.

Categories