Basic resolve for Angular routing - javascript

I'm creating a very basic login system with Angular JS. Basically, after a login form is submitted, credentials are checked against a database using PHP and Mysqli. If all is ok, it returns successful and I set a variable like $rootScope.isLoggedIn = true and user details in other similar variables.
Now I'm trying to work out the best way to use resolve with my routes so that only logged in users or users with correct permissions can access a page (including on page reload!)
I just can't get my head around how these resolves work. My routing looks like this so far (the dashboard page needs user to be logged in):
app.config(function($routeProvider) {
$routeProvider
//login
.when('/', {
templateUrl : 'framework/views/login.html',
controller : 'LoginCtrl',
title: 'Admin Login'
})
//dashboard
.when('/dashboard', {
templateUrl : 'framework/views/dashboard.html',
controller : 'DashboardCtrl',
title: 'Dashboard',
resolve: { ? }
})
//catch all redirect
.otherwise({ redirectTo: '/' });
});

Resolve will open your route with a resolved variable (that can be injected into controller) as soon as it gets value (instantly or when returned promise will be resolved). See:
resolve: {
myVar: function(service, $rootScope, etc){
return service.obj; // or primitive, or promise or something else
}
}
.controller('DashboardCtrl', function(myVar){
console.log(myVar); // that service.obj goes here
}
That can't prevent route from loading (as I understand, please correct me if I'm wrong).
For your case it's better to use something like this:
.when('/', {
templateUrl : 'framework/views/login.html',
controller : 'LoginCtrl',
title: 'Admin Login',
authenticate: false
})
.when('/dashboard', {
templateUrl : 'framework/views/dashboard.html',
controller : 'DashboardCtrl',
title: 'Dashboard',
authenticate: true
})
$rootScope.$on("$routeChangeStart", function(event, toState){
if (toState.authenticate && !$rootScope.isLoggedIn){
$location.path('/');
}
});

Related

Handling authentication with angular front-end -> How to protect specific pages?

Looking to build web app in Node.js with ability for user to log in (authentication), which has 3 non secure pages (/home, /contact, /about) and one secure page (/admin). As an aside, I've been referencing the scotch.io Mean Machine book.
The issue I'm having is that I've build everything out, and the login mechanism works in that when I log in, I get directed to /admin; however, when I go to /admin in the URL without logging in, I can still access the page. I.e. I'm not sure where to put the actual protection.
A bit below on how I've laid out my app. Hoping for as much a conceptual answer to suggest how I should be doing things, rather than necessarily only a code answer.
Services:
auth service posts to server the inputted username/password and returns either false or success (with user info and JWT token)
auth service also injects as AuthInterceptor the token (if there is one) into each HTTP header
Router:
angular.module('routerRoutes', ['ngRoute'])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/home.html',
controller: 'homeController',
controllerAs: 'home'
})
.when('/about', {
templateUrl: 'views/about.html',
controller: 'aboutController',
controllerAs: 'about'
})
.when('/contact', {
templateUrl: 'views/contact.html',
controller: 'contactController',
controllerAs: 'contact'
})
.when('/login', {
templateUrl: 'views/login.html',
controller: 'adminController',
controllerAs: 'login'
})
.when('/admin', {
templateUrl: 'views/admin/admin.html',
controller: 'adminController',
controllerAs: 'admin'
});
$locationProvider.html5Mode(true);
});
Controllers:
homeController, aboutController, contactController are generally empty for now
adminController:
.controller('adminController', function($rootScope, $location, Auth) {
var vm = this;
vm.loggedIn = Auth.isLoggedIn();
$rootScope.$on('$routeChangeStart', function() {
vm.loggedIn = Auth.isLoggedIn();
window.alert(vm.loggedIn); // this gives correct answer and works
Auth.getUser()
.success(function(data) {
vm.user = data;
});
});
vm.doLogin = function() {
vm.error = '';
Auth.login(vm.loginData.username, vm.loginData.password)
.success(function(data) {
vm.user = data.username;
if (data.success)
$location.path('/admin');
else
vm.error = data.message;
});
};
vm.doLogout = function() {
Auth.logout();
vm.user = {};
$location.path('/login');
};
});
And finally, below is my index.html (just the body):
<body class="container" ng-app="meanApp" ng-controller="adminController as admin">
<i class="fa fa-home">Home </i>
<i class="fa fa-shield">About </i>
<i class="fa fa-comment">Contact</i>
<i class="fa fa-comment">Admin</i>
<ul class="nav navbar-nav navbar-right">
<li ng-if="!admin.loggedIn">Login</li>
<li ng-if="admin.loggedIn" class="navbar-text">Hello {{ admin.user.username }}</li>
<li ng-if="admin.loggedIn">Logout</li>
</ul>
<main>
<div ng-view>
</div>
</main>
</body>
I won't paste the other html pages that get injected into since there isn't anything on them yet (the login.html has just the two input fields and submit button).
So a couple of questions:
In my index.html, when I click on /admin, it takes me to the admin page even if I'm not logged in. Where should I put the protection for that to not happen?
Any general comments on my setup and best practices that I'm not following?
Another nit:
I read that "li ng-if=" wouldn't show up in 'view source' if that branch of the decision tree wasn't hit, but it does. Was I misled or am I doing something wrong?
I took a custom property route to secure the routes in my application. Every state change taking place is listened for and inspected if it has this property. If it has this property set then it checks if user is logged in, if they are not, it routes them to the 'login' state.
I used UI-ROUTER in my current project where I have implemented this. I made a custom parameter called "data" that I used within the route.
Within a .config block to declare my opening routes:
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'login/login.html',
controller: 'LoginController',
controllerAs: 'vm'
})
.state('home', {
url: '',
templateUrl: 'layout/shell.html',
controller: 'ShellController',
controllerAs: 'vm',
data: {
requireLogin: true
}
})
Then I add this to a .run on the application where I'm looking for ui-router's $stateChangeStart event and looking at my custom property ('data') on the state declaration:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
var requireLogin = toState.hasOwnProperty('data') && toState.data.requireLogin;
if (requireLogin && !authService.isLoggedIn()) {
event.preventDefault();
authService.setDestinationState(toState.name);
$state.go('login');
}
if (toState.name !== 'login') {
authService.setDestinationState(toState.name);
}
});
In case you're wondering what the authService.setDestinationState does... it preserves the URL that the user was attempting to visit... once they successfully login it forwards them to that state automagically (see below):
function login() {
authService.authLogin(vm.credentials)
.then(loginComplete)
.catch(loginFailed);
function loginComplete(data, status, headers, config) {
vm.user = data;
$rootScope.$broadcast('authorized');
$state.go(authService.getDestinationState());
}
function loginFailed(status) {
console.log('XHR Failed for login.');
vm.user = undefined;
vm.error = 'Error: Invalid user or password. ' + status.error;
toastr.error(vm.error, {closeButton: true} );
}
}
When you define your Admin route, you can define a property called resolve. Each property within resolve is a function (it can be an injectable function). This function should return a promise, the promise's result can be injected into the controller.
For more information on resolve, look at http://odetocode.com/blogs/scott/archive/2014/05/20/using-resolve-in-angularjs-routes.aspx.
You can use resolve as follows to do an authentication check.
var authenticateRoute = ['$q', '$http' , function ($q, $http) {
var deferred = $q.defer();
$http.get("http://api.domain.com/whoami")
.then(function(response) {
if (response.data.userId) deferred.resolve(response.data.userId);
else window.location.href = "#/Login"
});
return deferred.promise();
}]
// ...
.when('/admin', {
templateUrl: 'views/admin/admin.html',
controller: 'adminController',
controllerAs: 'admin',
resolve: {
authenticatedAs: authenticateRoute
}
});
With this you could pass the authenticated User Id through - even if null - and let the controller deal with it, if for instance, you want a contextual message.
Else, you could do as above and only do so if there is a user Id from the authentication request, otherwise redirect to your login route.
Hope this helps! /AJ

Submitting a function before changing a route with angularJS

i would like to submit an http request to check if the user is logged in each time a route is changed.
is this possible with the latest version of angular ( 1.4.5 ).
my routes are like this:
// configure routes
app.config(['$routeProvider', function($routeProvider){
$routeProvider
// route for the home page
.when('/', {
templateUrl : 'directives/about.html',
controller : 'mainController'
})
// route for the home page
.when('/login', {
templateUrl : 'directives/login.html',
controller : 'loginContoller'
})
// route for the about page
.when('/about', {
templateUrl : 'directives/about.html',
controller : 'aboutController'
})
// route for the contact page
.when('/contact', {
templateUrl : 'directives/contact.html',
controller : 'contactController'
});
}]);
If you look a the docs you will see what arguments the when() method takes.
The route agument is an object and one property is called resolve.
The resolve property is an map of dependencies that should be resolved.
$routeProvider.when('/', {
templateUrl: 'directives/about.html',
controller: 'mainController',
resolve: {
loginCheck: function (myLoginCheckService) {
return myLoginCheckService();//could return a $q.promise object.
}
}
});
resolve - An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated. If all the promises are resolved successfully, the values of the resolved promises are injected and $routeChangeSuccess event is fired. If any of the promises are rejected the $routeChangeError event is fired.
AngularJS Docs
Look at the docs for more information.
create a helper method
function auth(configs){
// write your auth code here
return configs;
}
At the router would be like this.
.when('/', auth({
templateUrl : 'directives/about.html',
controller : 'mainController'
}))
Can do it by adding routeChangeStart inside run block of Angularjs
app.run(function($rootScope, $location) {
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
if ($rootScope.loggedInUser == null) {
// no logged user, redirect to /login
if ( next.templateUrl === "partials/login.html") {
} else {
$location.path("/login");
}
}
});
});

AngularJS: Preventing user from accessing a certain state

I'm building an app using sails.js backend and angular in frontend. I'm trying to prevent the user from accessing the admin control page if he's not authorized. I've run into couple of answers already, but none of them seem to fully work.
At the moment in my app.js, I have
$stateProvider
.state('home', {
url: "/home",
templateUrl: "home/homeTemplate.html",
controller: 'homeController'
})
.state('adminPage', {
url: "/adminPage",
templateUrl: "adminPage/adminTemplate.html",
controller: 'adminPageController',
resolve: {
validate: function($q, $sails, $location) {
var defer = $q.defer();
$sails.get("/user/getCurrentUser")
.success(function(response) {
if (response.user.accessAdminPage) {
defer.resolve();
}
else {
defer.reject("Access blocked");
$location.path('/');
}
return defer.promise;
})
}
}
})
The current code is partially working; The problem at the moment is, that when the unauthorized user first logs in and lands on the home page, and then accesses localhost:1337/#/adminPage, he actually reaches the page. The url in the address bar changes to localhost:1337/#/home but the user isn't redirected. Now the weird part is, when accessing the home page afterwards through the navbar and trying to access the admin page again, the user IS redirected to the home page as intended (although there's an annoying 'flash' while the page is reloaded).
For other people asking, this kind of handling has worked, and I'm wondering what I may have missed and generally any reasons for why my current solution isn't working.
You are returning promise from success function, this will never work.
You should return defered.promise (promise object) from outside success function.
CODE
$stateProvider
.state('home', {
url: "/home",
templateUrl: "home/homeTemplate.html",
controller: 'homeController'
})
.state('adminPage', {
url: "/adminPage",
templateUrl: "adminPage/adminTemplate.html",
controller: 'adminPageController',
resolve: {
validate: function($q, $sails, $location) {
var defer = $q.defer();
$sails.get("/user/getCurrentUser")
.success(function(response) {
if (response.user.accessAdminPage) {
defer.resolve();
} else {
defer.reject("Access blocked");
$location.path('/');
}
});
return defer.promise;
}
}
});
Hopefully this could help you, Thanks.
With the solution given by pankajparkar, the issue is that you will have to reply the logic in each state declaration. I recommend you to check the user's authorization in the onStateChangeStart event
angular.module('myApp', ['ui.router'])
.run(function($rootScope, AuthService){
$rootScope.$on('$stateChangeStart',
function(event, next, nextParams, prev, prevParams) {
AuthService.isNotAutorized()
.then(function() {
event.preventDefault();
$state.go('defaultState');
});
});
});

Angular one resolve for all states

I got a routeProvider for my states.
$routeProvider.
when("/register",{
templateUrl: "templates/register.html",
controller: "RegisterCtrl",
resolve: {
user: function(Auth) {
return Auth.resolveUser();
}
}
}).
when("/home",{
templateUrl: "templates/home.html",
controller: "HomeCtrl",
resolve: {
user: function(Auth) {
return Auth.resolveUser();
}
}
}). .... [.....]
Every state got a promise which resolves, when user-state is loggedIn. Then the code of the different controllers is executed. Now I want to have a mainController for the navigation bar, which should be present on all sites. The controller needs the userdata for checking for new messages etc.
Now: how is it possible to define the resolve globally in a root state (so i can access the userdata in the root controller for all sites) and all the other controllers execute their code only, if the promise from this roote state is resolved?
I hope I formulated my question understandable...
I think you're looking for something like $routeChangeStart, that is a way to execute something you want everytime the user changes his route inside your web app. Take a look at Route and this other question from stackoverflow. Hope it helps.
You can do this by defining your routes outside of the $routeProvider.when statements:
var routes = [
{
url: "/register",
config: {
templateUrl: "templates/register.html",
controller: "RegisterCtrl"
}
},
{
url: "/home",
config: {
templateUrl: "templates/home.html",
controller: "HomeCtrl"
}
}
];
Then iterating through your routes to extend the resolve property before registering them with the $routeProvider:
angular.forEach(routes, function (route) {
var url = route.url;
var routeConfig = route.config;
routeConfig.resolve = angular.extend(routeConfig.resolve || {}, {
// add your global resolves here
user: function(Auth) {
return Auth.resolveUser();
}
});
$routeProvider.when(url, routeConfig);
});
Your Auth.resolveUser() should be responsible for returning the fulfilled promise if it was already resolved previously.

UI-Router Resolve In Infinite Loop On Promise Failure

I'm trying to redirect users based on authentication so within the resolve of each route state I've placed my service for checking for authentication so in this case login and dashboard.
.state('login', {
url: "/login",
abstract: true,
templateUrl: '/app/login/login.html'
})
// Default login state
.state('login.index', {
url: "",
templateUrl: "/app/login/views/login.html",
controller: 'LoginController',
controllerAs: 'loginCtrl',
resolve: {
UserAuth: session
}
})
.state('dashboard', {
url: "/dashboard",
abstract: true,
templateUrl: '/app/dashboard/dashboard.html',
resolve: {
UserAuth: session
}
})
// Default dashboard state
.state('dashboard.index', {
url: "",
templateUrl: '/app/dashboard/views/page1.html',
controller: 'Page1Controller',
controllerAs: 'page1Ctrl',
resolve: {
UserAuth: session
}
})
// with multiple states like the child route above...
Within my .config for routing I also set up session for use above, which on promise success or existence of a session $state.go to dashboard, else on promise failure $state.go to login.
.config(function($stateProvider, $urlRouterProvider) {
var session = ['$q', '$location', '$state', 'SessionService',
function( $q, $location, $state, SessionService ) {
return SessionService.session()
.then(function( session ) {
// Check location path as $state is not resolved yet
var location = $location.path();
/**
* Check user authentication to dashboard and handle
* state change if session already exists
*/
if( location.indexOf('login') === 1 ) {
// Redirect to default dashboard view
$state.go('dashboard.index');
}
}, function( error ) {
// Check location path as $state is not resolved yet
var location = $location.path();
/**
* Check for unauthorized access to dashboard and handle
* state change if not located on login view
*/
if( location.indexOf( 'dashboard' ) === 1 ) {
// Redirect to default login view
$state.go('login.index');
}
// Don't propagate error since it has been handled and it prevents page
// return $q.reject( error );
});
}];
But, it's running through an infinite loop of 403 errors when I let the session time out, which is the correct server response for promise failure. It enters the if statement, and runs $state.go('login.index'), but keeps looping and never replaces the state with login. Can anyone see what I've done wrong? Works in all cases except session time out, then doesn't redirect.
I dropped together some code that I used in a past project that displays how I setup Authentication and interception of 401/403 on Angular side of things.
https://gist.github.com/jfornoff/4637069c398dc2f5b881
Note that especially the authorization of users can not be reliably done in the frontend and always have your backend authenticate and not give out data blindly to some end-point that your frontend should be using ;-) But I guess that's pretty self-explanatory.
Hope I could help you out!
Bye,
Jan

Categories