I've made an AuthService that should help me take care of all authentication matters like logging in, registering, getting user data from server etc.
I am looking for a solution that runs only on login, on page refresh and when triggered to refresh get the user data from the service and make it available on the controllers that i include the service to. I would like to have something like vm.user = AuthService.getUserData() and this returns the session variable from the service. Something like this:
function getUserData(){
if (session) {
return session.user;
}
return null;
}
On $rootScope.$on('$stateChangeStart' i have :
AuthService.loadSessionData();
Which translates to:
function loadSessionData() {
$http({
url: API.URL+'auth/session-data',
method: 'GET'
})
.success(function(response)
{
session = response;
})
.error(function(err){
console.log(err);
});
};
One of the issues here are that i have to set a timeout on AuthService.getUserData() because when this executes, the call that retrieves the session data from the server is not finished yet, but this is a bad practice.
Here is the complete service code http://pastebin.com/QpHrKJmb
How about using resolve? If I understood correctly you wish to have this data in your controller anyway.
You can add this to your state definitions:
.state('bla', {
template: '...'
controller: 'BlaCtrl',
resolve: {
user: ['AuthService', function(AuthService) {
return AuthService.loadSessionData();
}
}
}
also, alter your loadSessionData function: (make sure to inject $q to AuthService)
function loadSessionData() {
return $q(function(resolve, reject) {
$http({
url: API.URL + 'auth/session-data',
method: 'GET'
})
.success(function(response)
{
if (response) {
resolve(response);
} else {
reject();
}
})
.error(function(err){
reject(err);
});
})
}
Lastly, add the user object from the resolve function to you controller:
app.contoller('BlaCtrl', ['$scope', 'user', function($scope, user) {
$scope.user = user;
}]);
What does this accomplish?
In case the user does not have a valid session or an error occurs, the state change is rejected and the event $stateChangeError is thrown. You can listen (like you did for $stateChangeStart for that event and respond with a modal, redirect to an error page, or whatever.
You only pull the session for states that needs it - not for every state change.
The state is not resolved until the user data is resolved as well (either by resolve or reject).
You should call loadSessionData() in getUserData()
function getUserData(){
loadSessionData()
.success(function(response)
{
if (response) {
return response.user;
}
})
return null;
}
and
function loadSessionData() {
return $http.get(API.URL+'auth/session-data');
};
Related
My angular application is divided into 4 modules and all modules require user details so I am calling getUser method from each of the module. So when my application loads all 4 modules hit the getUser API simultaneously resulting in 4 get requests on server. How can I prevent this ? I am using singleton pattern in my getUser method so once my user gets loaded it will simply serve the user from an object. But that does not solves the problem if all modules request for the user simultaneously.
My code looks like this
getUser() {
let defer = this.q.defer();
if (!this.user) {
this.http.get(`${this.config.apiHost}users`)
.success(result => {
this.user = result;
this.rootScope.$broadcast('userFound', this.user);
defer.resolve(this.user);
})
.error(err => defer.reject(err))
}
else {
defer.resolve(this.user);
this.rootScope.$broadcast('userFound', this.user);
}
return defer.promise;
}
By storing the current request in a variable the call to UserService.get will return the same request promise.
Then when the promise resolves, it will resolve to all your modules.
angular.module('app').service('UserService', function ($http) {
var self = this;
var getRequestCache;
/**
* Will get the current logged in user
* #return user
*/
this.get = function () {
if (getRequestCache) {
return getRequestCache;
}
getRequestCache = $http({
url: '/api/user',
method: 'GET',
cache: false
}).then(function (response) {
// clear request cache when request is done so that a new request can be called next time
getRequestCache = undefined;
return response.data;
});
return getRequestCache;
};
});
You are using ui-router for the routing. You can then use this to resolve the user when landing on the page.
In your routing config :
$stateProvider
.state('myPage', {
url: '/myPage',
templateUrl: 'myPage.html',
controller: 'myCtrl',
resolve: {
userDetails: ['UserService', function(UserService) {
return UserService.getUserDetails();
}],
}
})
In your controller
angular.module('myModule')
.controller('myCtrl', ['userDetails', function(userDetails) {
console.log(userDetails);
}];
This will load the user details while loading the page.
I solved this problem by using defer object as a global object so that it only gets initialised once.
Can you tell me what is the correct way to redirect to another page if $http.post returns a specific error code.
Just to add context, I want to redirect to another page if the user is not logged in or authorized to use the API.
function getData(filter) {
var deferred = $q.defer();
var data = JSON.stringify(filter);
$http.post('/myapp/api/getData', data)
.success(function (data, status, headers, config) {
deferred.resolve(data);
})
.error(function (error) {
deferred.reject(error);
});
return deferred.promise;
}
You could do a redirect to the page using $window.location.href, based on the error condition you have.
var app = angular.module("sampleApp", []);
app.controller("sampleController", [
"$scope",
'$window',
'sampleService',
function($scope, $window, sampleService) {
sampleService.getData().then(function(result) {}, function(error) {
if (error.statusCode === 400) {
alert("Error");
$window.location.href = "http://stackoverflow.com"
}
});
}
]);
app.service("sampleService", function() {
this.getData = function() {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
reject({
statusCode: 400
});
}, 1000);
});
return promise;
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-App="sampleApp">
<div ng-controller="sampleController">
</div>
</div>
The best way to catch global AuthenticationErrorin angular is with interceptor.
This way you can monitor all request that are sent from angular and check for AuthenticationError.
$provide.factory('AuthErrorInterceptor', function($q, $location) {
return {
'responseError': function(rejection) {
//check for auth error
$location.path('/login');
return $q.reject(rejection);
}
};
});
Example :
$http.post('/myapp/api/getData', data)
.then(function (data) {
if(data.ErrorCode==1)
{
$window.location.href="controllerName/actionName";
}
})
Use a interceptor service in order to centralize all of your rejection request in the same service.
module.config(['$httpProvider', ($httpProvider: ng.IHttpProvider) => {
$httpProvider.interceptors.push('errorService');
}]);
module.factory('errorService', ['$location', function($location) {
var errorService = {
responseError: function(rejection) {
if (rejection === '401') {
$location.path('/login');
}
}
};
return errorService;
}]);
The $http.post is misguiding.
So far the best answer is #Kliment's. Interceptors are the best way to manage what comes before and after http requests.
However, if your end goal is to prevent access to a page, you have to at least use a routing plugin (ngRoute, ui-router) because with the promise idea there will always be a delay between the http request and the response.
Depending on server response time you'll still see the page display for about a second or so.
With ui-router you simply configure a resolve method for each state you want to protect. It could look like this:
.state('protected',
{
url : '/protected_page',
templateUrl : 'secret.html',
resolve: {
loggedin: loggedin
}
})
loggedin refers to a function you define that contains your $http.post call (or better yet a service)
function loggedin($timeout, $q, $location, loginService) {
loginService.then(function(data) {
if(data.status == 401) {
//$timeout(function() { $location.path('/login'); });
return $q.reject();
} else {
return $q.when();
}
});
}
Here this particular service returns a 401 status but you can return anything.
The state will not be resolved (and the page not displayed) until it's accepted or rejected.
You can redirect directly from there if you want, although it's not very elegant.
ui-router gives you another option with default redirection:
if (tokenIsValid())
$urlRouterProvider.otherwise("/home");
else
$urlRouterProvider.otherwise("/login");
With otherwise you tell ui-router to go to certain urls if no state exists for a particular request or if a resolve has been rejected.
On another subject, your http request is badly written.
.success and .error are deprecated and you don't need to create a promise ($q) over an $http request that itself already returns a promise.
You have a good example in the documentation linked above.
You can redirect to page on unauthorized access of a user based on the status code which you can return from your API call.
$http({
method: "POST",
url: 'Api/login',
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}
}).success(function (data,status) {
if(status==200){
alert('Successfully logged in');
$location.path('/dashboard'); //You can use this if you are defined your states.
}
}).error(function (data,status) {
if(status==403||status==403){ //Based on what error code you are returning from API
$location.path('/login');// if states are defined else
$window.location.href = "https://www.google.com";
}
});
First of all Nice Question , In this scenario You Can use $location , $state If it is external url You can use $window.location.href ... I would recommend $location and it is the best way ...
Please See the link for further Using $window or $location to Redirect in AngularJS
function getData(filter) {
var deferred = $q.defer();
var data = JSON.stringify(filter);
$http.post('/myapp/api/getData', data)
.success(function (data, status, headers, config) {
deferred.resolve(data);
if(data.errorcode==9999) // Define Your Error Code in Server
{
$location.path('/login'); // You Can Set Your Own Router
} })
.error(function (error) {
$location.path('/login'); // You Can Set Your Own Router
deferred.reject(error);
});
return deferred.promise;
}
Preferably use $location or $state ...
I have authentication set up in such a way that I want to prevent any route/state from loading until I know that the user is authorized to access the page. If they are, then the requested page should load, if not, then it should go to the login page.
My config function:
$stateProvider
.state('login', {
url: '/login',
templateUrl : 'login.html',
controller : 'loginController',
data: {
authorizedRoles: [USER_ROLES.guest]
}
})
.state('home', {
url: '/',
templateUrl : 'home.html',
controller : 'homeController',
data: {
authorizedRoles: [USER_ROLES.admin]
}
})
.state('admin', {
url: '/admin',
templateUrl : 'admin.html',
controller : 'adminController',
data: {
authorizedRoles: [USER_ROLES.admin]
}
});
$urlRouterProvider.otherwise('/login');
$locationProvider.html5Mode(true);
My run function:
$rootScope.$on('$stateChangeStart', function(event, next) {
event.preventDefault();
function checkAuthorization() {
if(!AuthService.isAuthorized(authRole)) {
$state.go('login');
} else {
$state.go(next.name);
}
}
if(AuthService.getRoleId() === undefined) {
// We'll have to send request to server to see if user is logged in
AuthService.checkLogin().then(function(response) {
checkAuthorization();
});
} else {
checkAuthorization();
}
})
If I keep the event.preventDefault() in my run function, then the app will be stuck in a loop always going to the requested state. If I remove the event.preventDefault() statement then the app will load the view (which will be visible for a second) before realizing the user should not be allowed to view it (and then go to the correct state).
How can I solve this problem?
You should use resolve and make request to the server to see if user is logged in the resolve
https://github.com/angular-ui/ui-router/wiki#resolve
.state('whatever',{
...
promiseObj: function($http){
// $http returns a promise for the url data
return $http({method: 'GET', url: '/someUrl'}).$promise;
},
...
}
OR
if you have make a call in the controller, make the call in resolve in state, in which your api should response with a 401 if user is not login in and redirect to the log in screen if you have an intercept service.
There is detailed explanation how to do this kind of resolve/wait stuff in this Q & A with working plunker.
An extracted version of the Auth service is:
.factory('userService', function ($timeout, $q) {
var user = undefined;
return {
// async way how to load user from Server API
getAuthObject: function () {
var deferred = $q.defer();
// later we can use this quick way -
// - once user is already loaded
if (user) {
return $q.when(user);
}
// server fake call, in action would be $http
$timeout(function () {
// server returned UN authenticated user
user = {isAuthenticated: false };
// here resolved after 500ms
deferred.resolve(user)
}, 500)
return deferred.promise;
},
// sync, quick way how to check IS authenticated...
isAuthenticated: function () {
return user !== undefined
&& user.isAuthenticated;
}
};
})
where the most important parts are
var user = undefined; - "global" variable which is
containing user and can answer if he has rights
does not contain user (yet) and the answer is "not authorized" then
returned service function: isAuthenticated
That should solve the issue. check more details here
There is a hidden page in my angular app, so only special id user can access it. To implement it I send HTTP get request to the server (because only the server knows that special id) when state changes to that hidden state.
And if server responses to me with an error message then I prevent state changing process, in other case user goes to that special page.
Here is the code I am using:
angular
.run(["$rootScope", "$location", "$state", "mySvc", function ($rootScope, $location, $state, mySvc) {
var id = mySvc.getId();
$rootScope.$on( '$stateChangeStart', function(event, toState , toParams, fromState, fromParams) {
if(toState.name === "specialstate") {
mySvc.check(id)
.then(function(result) {
if(result.error !== 0) {
event.preventDefault();
}
}, function(error) {
event.preventDefault();
});
}
});
}]);
Function in service:
function check(id) {
var deferred = $q.defer();
$http({
url: "/api/url",
method: "GET",
headers: {'Content-Type': 'application/json'}
}).
then(function (result) {
console.log(result);
if(result.data.error === 0) {
deferred.resolve(result.data);
}
else {
deferred.reject(result.data);
}
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
};
Everything is right, except one thing: the state is being changed without waiting for request result. I think I should use resolve, but I do not know how to use it properly.
Thanks.
ui-router supports a resolve object that defines aset of data that should be resolved (from server, for instance), before the state transition is allowed.
.state('hidden', {
url: '/hidden',
...
resolve: {
data: function(mySvc) {
var id = mySvc.getId();
return mySvc.check(id).then(function(result) {
if (result.error !== 0) {
throw new Error("not allowed");
}
});
}
}
});
The data object defined would be available to your controller in that hidden state, and only in case it returns a 2xx status code and there's an error property equal to 0.
If data is resolved, then a state change occurs and a controller is instantiated. In case of rejection, none of this takes place.
As a personal note, I find that a great device to handle authorization.
I've a factory named as 'userService'.
.factory('userService', function($http) {
var users = [];
return {
getUsers: function(){
return $http.get("https://www.yoursite.com/users").then(function(response){
users = response;
return users;
});
},
getUser: function(index){
return users[i];
}
}
})
In the first page, On button click I want to call getUsers function and it will return the 'users' array.
I want to use 'users' array in the second page. How can I do it?
P.s: I'm using getters and setters to store the response in first page and access the same in second page. Is this the way everyone doing?
1). getUsers. For consistence sake I would still use the same service method on the second page but I would also add data caching logic:
.factory('userService', function($q, $http) {
var users;
return {
getUsers: function() {
return users ? $q.when(users) : $http.get("https://www.yoursite.com/users").then(function(response) {
users = response;
return users;
});
},
getUser: function(index){
return users[i];
}
};
});
Now on the second page usage is the same as it was on the first:
userService.getUsers().then(function(users) {
$scope.users = users;
});
but this promise will resolve immediately because users are already loaded and available.
2). getUser. Also it makes sense to turn getUser method into asynchronous as well:
getUser: function(index){
return this.getUsers().then(function(users) {
return users[i];
});
}
and use it this way in controller:
userService.getUser(123).then(function(user) {
console.log(user);
});
Here is the pattern i have followed in my own project. You can see the code below
.factory('userService', function($http) {
return {
serviceCall : function(urls, successCallBack) {
$http({
method : 'post',
url : url,
timeout : timeout
}).success(function(data, status, headers, config) {
commonDataFactory.setResponse(data);
}).error(function(data, status, headers, config) {
commonDataFactory.setResponse({"data":"undefined"});
alert("error");
});
}
},
};
After service call set the response data in common data factory so that it will be accessible throughout the app
In the above code I've used common data factory to store the response.