Angular.js: page flickering on authentication redirect - angularjs

I'm implementing some simple client-side authentication logic in Angular.js. The pages involved are:
/account#/login (public)
/account (require login)
/account#/settings (require login)
When a user is not logged in and try to visit either /account or /account/#/settings, the app is supposed to redirect to the login page.
I have the following routes configured using ui-router:
$stateProvider
.state('overview', {
url: '/',
restricted: true
})
.state('settings', {
url: '/settings',
restricted: true
})
.state('login', {
url: '/login',
restricted: false
})
and upon URL change, I check if the upcoming page is a restricted page and whether the current user is not logged in. If so redirect to login.
app.run(function($rootScope, $location, $state, auth) {
$rootScope.$on('$stateChangeStart', function(event, next) {
if (next.restricted && !auth.isLoggedIn()) {
event.preventDefault();
$state.go('login');
}
});
});
auth is just a service that checks the login status and returns either true (logged in) or false (not logged in).
Here's my question:
Even though this (kind of) works, I see a page flickering issue when trying to visit a restricted page while not logged in. The page flashes the contents of the restricted page quickly before redirecting me to the login page.
I did a little bit researching online and some people have mentioned the potential solution could be using resolve when defining my states, since the page won't load unless it resolves successfully. However, when I try to add
resolve: {
load: function(auth) {
return auth.isLoggedIn();
}
}
It didn't work. What am I missing? Is using resolve the way to go?

The way you are currently doing it will check if the user is logged in or not and set load to true or false. Also controller gets instantiated before load is resolved which is why you see the flickering. You need to achieve two things here:
Make sure that load is resolved before the controller is instantiated.
If user is not logged in, redirect the user to the login page.
For the first part we need to use a promise as it will be resolved and converted to value before controller is instantiated. This is what the documentation says:
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.
Following code can do that for us:
var isLoggedin = ['auth', '$q',
function(auth, $q) {
var deferred = $q.defer();
//assuming auth.isLoggedIn returns a promise
var loginPromise = auth.isLoggedIn();
loginPromise.then(
function(response) {
deferred.resolve(response);
},
function(error) {
deferred.reject('Not logged in');
});
return deferred.promise;
}
];
And states will use isLoggedin:
$stateProvider
.state('overview', {
url: '/',
resolve: {
loggedin: isLoggedin
}
})
.state('settings', {
url: '/settings',
resolve: {
loggedin: isLoggedin
}
})
.state('login', {
url: '/login'
})
For the second problem, that is redirecting the user to login page, you can listen to $stateChangeError event which is fired in case the state is not resolved, and use $state.go to redirect the user.

Related

AngularJS and ui-router: wrong routes behaviour after refresh

I'm developing an application with AngujarJS and ui-router and I'm having a problem loading the routes. To facilitate the explanation I will delete parts of the code. My rotes file looks like this:
app.config(function($stateProvider, $urlRouterProvider,
$locationProvider) {
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl : 'pages/dashboard.html',
controller : 'DashController'
})
.state('dashboard.vendas', {
url: '/vendas',
templateUrl : 'pages/vendas.html',
controller : 'vendasController'
})
;
$urlRouterProvider.otherwise('/');
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
});
I load this route /dashboard after the user login. On this route, I load, among other things, an http method that returns the companies that the user will have access to, through the factory below:
app.factory("factoryEmpresas",
function($http,urlBase,isAuthenticated) {
return {
getEmpresas: function() {
return $http({
url: urlBase.getUrl() + '/empresas',
method: "GET",
headers: {
'X-Token': isAuthenticated.getJWT()
}
}).then(function successCallback(response) {
return response.data;
}, function errorCallback(response) {
return response;
});
}
}
});
When accessing the route /vendas (which is related to the state dashboard.vendas), I make another http call using a variable that is in the factoryEmpresas return as follows:
app.factory("factoryVendas",
function($http,urlBase,isAuthenticated) {
return {
getVendas: function(id_empresa) {
return $http({
url: urlBase.getUrl() + '/vendas?id_empresa=' + id_empresa,
method: "GET",
headers: {
'X-Token': isAuthenticated.getJWT()
}
}).then(function successCallback(response) {
return response.data;
}, function errorCallback(response) {
return response;
});
}
}
});
Everything works perfectly, until the user decides to refresh (F5) when on the route /vendas. At that moment the loading of the two routes /dashboad and /vendas is triggered. However, when making the http call at factoryVendas, the factoryDashboard has not yet returned the getEmpresas function. Thus, the variable "empresa_id" is not filled with the value that it should, thus resulting in an undefined call in place of "empresa_id".
The question I ask is, how can I make factorySales wait for the factoryDashboard to return so that I can make the call with the variable duly filled in?
Any help will be welcome.
Thank you!
It sounds like vendas has a direct dependency on empresa. Would it make sense to make empresaId part of the route parameters for accessing vendas? That way the information needed to load the page would always be accessible (from $stateParams), even on reload
.state('dashboard.vendas', {
url: '/:empresaId/vendas/',
templateUrl : 'pages/vendas.html',
controller : 'vendasController'
});
The other thing you could do is define load order by creating a resolve on the parent that the child consumes. Then uirouter knows it needs to wait for the parent resolve before attempting to load the child.
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl : 'pages/dashboard.html',
controller : 'DashController',
resolve: {
Empresas: function(factoryEmpresas) {
return factoryEmpresas.getEmpresas();
}
}
})
.state('dashboard.vendas', {
url: '/vendas',
templateUrl : 'pages/vendas.html',
controller : 'vendasController',
resolve: {
Vendas: function(Empresas, factoryVendas) {
var id_empresa = Empresas[0].id; // just kind of guessing here
return factoryVendas.getVendas(id_empresa);
}
}
})
Vendas and Empresas can be injected into their respective controllers.
While this would work I'd actually discourage this method because now your user is waiting around more for content to appear. Studies have shown its better to show your users incremental progress, your app will feel faster. I try to avoid resolves when possible and have initialization logic in my controller. That way, the page loads immediately and fills in as data becomes available.

AngularJS $state.go not working if user not logged in

Below i have a state in my routes.js in my angular routes controller. I want it to check if the user is an admin, and only if it is an admin it goes to admin, otherwise it is redirected to home page.
There are no errors on the console if the user is logged in and is an admin, also it redirects if the user is not an admin and logged in, but if a user is not logged in and not an admin i can still see the template of that page and it says POST http://localhost:3000/users/sign_in.json 401 (Unauthorized)
.state('adminpage', {
url: '/adminpage',
templateUrl: 'views/adminpage.html',
controller: 'AdminPageCtrl',
onEnter: function(Auth, $state){
Auth.currentUser().then(function( user){
if (user.isadmin) {
$state.go('adminpage');
}
else {
$state.go('home');
}
})
}
})
I assume Auth.currentUser() rejects the promise if user is not logged in. So, you need to define what should be done on promise rejection, i.e. if user is not logged in.
onEnter: function(Auth, $state){
Auth.currentUser().then(function( user){
if (user.isadmin) {
$state.go('adminpage');
}
else {
$state.go('home');
}
})
.catch(function() {
// Redirect the user somewhere else, or show an error here.
});
}
The .catch() method actually handles the rejection of the promise, and you can try to redirect the user somewhere else in this block. In this way, the restricted page won't be shown to the user.
Hope that helps.

Unable to retrieve the error associated and display in login page

Using AngularJS and Spring Security, when the authentication fails there is a redirect to the login page with error=true. I need to display this error on login page (AngularJS). I don't know how to do this.
Below is the declaration of authentication-failure-url in ssecurity-context.xml:
<security:form-login login-page="/login"
login-processing-url="/authenticate"
authentication-failure-url="/login?error=true"
username-parameter="username" password-parameter="password"/>
Could some please tell me how to retrieve the error and show on login page?
JS
angular.module('app.services')
.service('AuthenticateService'‌​,['$http','$location‌​',function($http,$lo‌​cation)
{
return
{
login : function(uname, pword)
{
var data = "username="+uname+"&password="+pword;
return $http.post("authenticate",data,
{
headers: { "Content-Type": "application/x-www-form-urlencoded" } })
.then(function (response)
{
console.log(response.status);
},function(error)
{
console.log(error.status);
});
}
}
}]);
When your login attempt fails, you're redirecting to /login/error/true
Then, you should have on your angularJS a route to that URL with the partial HTML that you want to display, alongside with an angularJS controller which will handle or do the appropriate actions upon that URL.
For example, let's say that your route file will look this:
var myApp = angular.module("myApp", [ "ngRoute" ]);
myApp.config(function ($routeProvider) {
$routeProvider
.when("/login/error/:errValue",
{controller: "LoginController", templateUrl: "/partials/login.html"})
});
Now all you have to do is tho check on LoginController whether :errValue is true or false. and act upon that value.
with wrong credentials, I tried checking the value of errValue in the controller, but its says undefined
Below are the routes declared
$stateProvider
.state("/login",{ //this for login page
url:'/login',
templateUrl:'./resources/js/baseapp/ContactLogin/views/contactLogin.html',
controller:'contactLoginCtrl'
})
.state("/home",{
url:'/home', // this is for home page
templateUrl:'./resources/js/baseapp/ContactHome/views/ContactHome.html',
controller:'contactHomeCtrl'
})
.state("/login:error/:errValue",{ // this is for login error page
url:'/home',
templateUrl:'./resources/js/baseapp/ContactLogin/views/contactLogin.html',
controller:'contactLoginCtrl'
})
$urlRouterProvider.otherwise("/login");

Angular Intercepting a particular http request and prompt for login

I am working on an angular app. It's an SPA. I am loading the profile(or myAccount) page into the home page when user clicks on a link.
<a href="#" ng-click="getProfileData()"/></a>
in my controller:
$scope.getProfileData(){
$http.get('/profile').then(
function success(){
},
function error(){
}
}
}
the link makes an ajax request through $http service of angular.
what I want to do is, when user clicks on the link, before making the ajax request to check if he's logged in or not. if not, open the sign in popup, and after sign in continue with the same request.
I've looked into $httpProvider.interceptors, but I'm still not sure how to do it, since I need to pause hold the $http request till the login is completed, modify the config by adding login details into it and then continue.
Is using of interceptors the correct option? If yes, how should I proceed towards my objective?
EDIT:
my primary concern is to continue with the same request. i.e. i don't want the user to click on profile link again after he has logged in. i can check if user is logged in or not before making the ajax request. if he is, then it's good. but if he's not, he has to log in (in the modal i pop up), and then he has to click on the profile link again. is there a way to continue the previous http request after login?
There are only three points in the application where the login modal should appear:
When you are on a welcome page and you click “Login”.
When you are not logged in and you attempt to visit a page that requires login.
When you attempt to make a request that requires a login(Ex:session expiration).
Determining which pages require a logged in user
Considering you are using ui router,you can secure routes of your application with the help of attaching additional properties to a route.Here we add a requireLogin property for each state.
app.config(function ($stateProvider) {
$stateProvider
.state('welcome', {
url: '/welcome',
// ...
data: {
requireLogin: false
}
})
.state('app', {
abstract: true,
// ...
data: {
requireLogin: true // this property will apply to all children of 'app'
}
})
});
For routes that do not require a login we set requireLogin to false.
Capturing attempted state changes
This is where we are going to capture the attempted state changes and inspect them for our requireLogin property.
app.run(function ($rootScope) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
var requireLogin = toState.data.requireLogin;
if (requireLogin && typeof $rootScope.currentUser === 'undefined') {
event.preventDefault();
$rootScope.returnToState = toState.url;
// get me a login modal!
}
});
});
As you can see, if the route requires a login and $rootScope.currentUser is not yet set, we will prevent the attempted state change and show the modal.
Redirecting to initially requested url after login
The last part requires redirecting to the initially requested url after login.In our app.run we have set a variable $rootScope.returnToState = toState.url which we can use in our login controller to redirect.In our controller for the login page we can do something like this:
$scope.login = function(form) {
$scope.submitted = true;
if(form.$valid) {
Auth.login({
email: $scope.user.email,
password: $scope.user.password
})
.then( function() {
// Logged in, redirect to correct room
if( $rootScope.returnToState) {
$location.path($rootScope.returnToState);
} else {
//redirect all others after login to /rooms
$location.path('/home');
}
})
.catch( function(err) {
$scope.errors.other = err.message;
});
}
};
For further reference please refer to this blog post http://brewhouse.io/blog/2014/12/09/authentication-made-simple-in-single-page-angularjs-applications.html
This can help you build a rock solid authorization for your app which I think you might be looking for.
Why can't you just make another function call to verify, if the user is logged-in. And based on that fire up the ajax request that you are trying up there. Something like
$scope.getProfileData(){
if($scope.isLoggedin()){
$http.get('/profile').then(
function success(){
},
function error(){
}
}
}
};
$scope.isLoggedin = function(){
// Do some processing and return true/false based on if user is logged-in
};

url routing with parameter in angular js

I have this routing configuration in app.js:
$stateProvider.state('home', {
url: '/',
views:
{
'contentView':
{
templateUrl:'modules/login/login.html',
controller:'loginCtrl'
}
},
data:
{
login: true
}
});
Whenever user hits the browser with URL http://.../MyClient/#/?param=ParamValue.
It will take the user to the login page and I am able to access the param value as well.
There is a logout button in the successive pages and after logout, I want to redirect to the initial URL and if I try something like
$location.path('/#/?param=ParamValue');
the user will stay on the same page and URL will be like this:
http://.../MyClient/#/%23/%3Fparam=ParamValue
Please let me know how to fix this.
You should try to use the $state service to navigate between your states, $state.go('home', {param: ParamValue})
also add the parameter you need in the url template for the state.
$stateProvider.state('home', {
url: '/?param',
views:
{ ....
https://github.com/angular-ui/ui-router/wiki/Quick-Reference#state-1

Resources