I’m developing a Single Page Application with AngularJS.
When a user successfully logs in, a security token is stored in a cookie. Now, when he refreshes the page, the token will be sent to the backend, which returns a JSON object "currentUser" containing all the relevant information about the current user (as name, access-groups, profile picture, etc.).
The problem is, this is an asynchronous process of course, so when the controller starts another operation, say, just alerting the user’s name, this value will be undefined at that time.
Of course, I could set a timeout but is there a better solution?
I thought about a "currentUserService", which initializes first (sending the cookie and filling the user information with the backend response) and can only be processed after this initialization is completed.
But how can this be done? Or are there any other possibilities?
edit:
Hi guys,
thanks for the input!
Both of your suggestions seem to be very promising for asynchronous requests in general, but I think they might not fit perfectly for my concern:
The information about the current user only have to be requested once, so I would like to store them for the whole application (e.g. in the rootScope or a service) accessible from any controller without having to request them again in every controller (as in the callback or resolve-solution) but make sure that there won’t be any „timeout“ problems. Do you have any ideas?
You can resolve the user's data before the view loads either with ng-route or ui-router:
This example is written for ui-router:
.state('profile', {
url: '/profile',
controller: 'profileCtrl as vm',
resolve: {
user: function(AuthService) {
//Return a promise or an object to be resolved.
return AuthService.getUserFromToken(); //Say this is asynchronous and returns a promise
}
}
});
//In controller:
.controller('profileCtrl', function(... , user) {
//User data available here
this.user = user;
});
Please note if any errors arise during the resolve stage the state will not be loaded so you'll have to take care of the errors!
If a user refreshes you have to initialize everything. I assume the token is stored in localstorage or something and I assume this is angular 1.*. To do this I think you should call user-related functions from your http call-callback:
$scope.user = {};
$scope.getUser = function(){
$http({
method: 'GET',
url: '/someUrl'
}).then(function (res) {
$scope.user = res.data; //or whatever the response is
$scope.handleUserRelatedThings();
}).catch(function(err) {
//handle error
})
}
$scope.handleUserRelatedThings = function(){
//do things with $scope.user
alert($scope.user.name);
}
//on init
$scope.getUser();
Related
I am trying to change the location after a promise is returned, however, the location will not change.
function register() {
firebase.auth().createUserWithEmailAndPassword($scope.email, $scope.password)
.then(function (user) {
console.log(user);
UserService.setUser(user);
$location.path('/home');
})
.catch(function (error) {
$scope.message = error.message;
alert($scope.message);
});
}
I'm assuming this is because it's within a promise's .then function, but I don't see documentation on $location that says it can't be used within a promise.
The console shows the user that was created and there are no errors.
From the AngularJS Documentation
When should I use $location?
Any time your application needs to react to a change in the current URL or if you want to change the current URL in the browser.
What does it not do?
It does not cause a full page reload when the browser URL is changed. To reload the page after changing the URL, use the lower-level API, $window.location.href.
The issue is related with the angular digest cycles.
To solve it, you can do:
$location.path('/home');
$rootScope.$apply();
Using Angular 1.5.5 here:
Is there any way to tell Angular to ignore response body for particular requests (such as $save)? It drives me crazy that after I call $save, angular updates the model with the object returned by a server, which initially was supposed to be used to distinguish between different resolutions of the request. It results in unwanted form clear. Interestingly enough, this behaviour remains even if I send a 400 or 500 http status code.
In case you need more info, relevant code is below.
Controller:
'use strict';
angular
.module('app.operators')
.controller('OperatorNewController', OperatorNewController);
OperatorNewController.$inject = ['operatorsService', 'notify'];
function OperatorNewController(operatorsService, notify) {
var vm = this;
vm.done = done;
activate();
function activate() {
vm.operator = new operatorsService();
}
function done(form) {
if (form.$invalid) {
// do stuff
return false;
}
vm.operator.$save(function(response) {
if (response.success && response._id) {
$state.go('app.operators.details', {id: response._id}, { reload: true });
} else if (response.inactive) {
// do stuff
} else {
// do other stuff
}
}, function (error) {
// do other stuff
});
}
}
Service:
'use strict';
angular
.module('app.operators')
.service('operatorsService', operatorsService);
operatorsService.$inject = ['$resource'];
function operatorsService($resource) {
return $resource('/operators/:id/', {id: '#_id'}, {
'update': { method: 'PUT' }
});
}
Server request handler is also fairly simple:
.post('/', function (req, res) {
if (!req.operator.active) {
return res.status(500).json({ inactive: true, success: false });
}
// do stuff
return res.json({ success: true });
});
In either way I don't like the idea of having to send the entire object from server (particularily when it's a failed request), and even if I have to, I still need a way to send some extra data that will be ignored by Angular.
Your help is very much appreciated!
The $save method of the resource object empties and replaces the object with the results of the XHR POST results. To avoid this, use the .save method of the operatorsService:
//vm.operator.$save(function(response) {
vm.newOperator = operatorsService.save(vm.operator, function(response),
if (response.success && response._id) {
$state.go('app.operators.details', {id: response._id}, { reload: true });
} else if (response.inactive) {
// do stuff
} else {
// do other stuff
}
}, function (error) {
// do other stuff
});
UPDATE
It results in unwanted form clear. Interestingly enough, this behaviour remains even if I send a 400 or 500 http status code.
This behavior is NOT VERIFIED.
I created a PLNKR to attempt to verify this behavior and found that the $save method does not replace the resource object if the server returns a status of 400 or 500. However it does empty and replace the resource object if the XHR status code is 200 (OK).
The DEMO on PLNKR
It drives me crazy that after I call $save, angular updates the model with the object returned by a server
It helps to understand how browsers handle traditional submits from forms.
The default operation for a submit button uses method=get. The browser appends the form inputs to the URL as query parameters and executes an HTTP GET operation with that URL. The browser then clears the window or frame and loads the results from the server.
The default operation for method=post is to serializes the inputs and place them in the body of an HTTP POST. The browser then clears the window or frame and loads the results from the server.
In AngularJS the form directive cancels the browser default operation and executes the Angular Expression set by either the ng-submit or ng-click directive. All $resource instance methods including $get and $save, empty and replace the resource object with XHR results from the server if the XHR is successful. This is consistent with the way browsers traditionally handle forms.
In RESTful APIs, HTTP GET operations return the state of a server resource without changing it. HTTP POST operations add a new resource state to the server. APIs usually return the new resource state, with additional information such as ID, Location, timestamps, etc. Some RESTful APIs return a redirect (status 302 or 303) in which case browsers transparently do an HTTP GET using the new location. (This helps to Solve the Double Submission Problem.)
When designing RESTful APIs, it is important to understand how traditional browsers behave and the expectations of RESTful clients such as AngularJS ngResource.
I have an AngularJS application and I want to cache the REST service responses. I found some libraries like angular-cached-resource which can do this by storing the data into the local storage of the web browser.
But sometimes I do some POST / PUT / DELETE REST calls and then some of the REST previously cached service responses need to be performed again. So it seems that it is possible to delete the cached responses then and the call will be sent to the server next time.
But what about if the server sends me in HTTP Header some values like the expires or the etag? I have to read the HTTP Header and react by myself or is there a library in AngularJS which can also handle this?
So if I should hit the server and not read the cache of the local storage is dependent on the HTTP Header Cache fields and if there are any PUT / POST / DELETE calls which have the response that for example "reload of every user settings element" are needed. So I have to take this response and create a map which tells me that for example REST services A, C and F (user settings related stuff) needs to hit the server again next time when they are executed or if the Cache expires from the HTTP Headers.
Is this possible with an AngularJS library or do you have any other recommendations? I think this is similar to Observer or PubSub Pattern, isn't it?
One more thing: Is it also possible to have something like PubSub without using a cache / local storage (so also no HTTP Header Cache controls)? So I can not call the REST service, because then it would hit the server, which I do not want in some circumstances (response from a previous REST call which returns me the event "reload of every user settings element").
You can try something like this.
app.factory('requestService', ['$http', function ($http) {
var data = {};
var service = {
getCall : funtion(requstUrl, successCallback, failureCallback, getFromCache){
if(!getFromCache){
$http.get(requstUrl)
.success(function(data){
successCallback(data);
data.requstUrl = data;
})
.error(function(){
failureCallback(data);
})
}else{
successCallback(data.requstUrl);
}
},
postCall : function(requestUrl, paramToPass, successCallback, failureCallback, getFromCache){
if(!getFromCache){
$http.post(requestUrl, paramToPass)
.success(function(data){
successCallback(data);
data.requstUrl = data;
})
.error(function(data){
failureCallback(data);
})
}else{
successCallback(data.requstUrl);
}
}
};
return service;
}]);
This is just a simple code I wrote to implement your concept. I haven't tested it and is all yours.
every time the route change, I check if is set a variable with the current logged user details, if this variable is not set, I simply redirect to the login page.
Now I'm trying to achieve some kind of "remember me" functionality, so every time the route changes, if the variable user doesn't exist instead of redirecting to the login page, I check the local storage for an "authtoken", if is set I call a check function passing the authtoken to the server, that returns the user and the app will works the same way as after the manual login (that returns the same user as the check function).
I'm pretty sure this is not the best way to do that.
If I reload the page, first thing I run the check function that sends the authtoken to the server and wait for a response, if the user exists that value is assigned to a variable in the rootscope.
I have different services that use the variable in the rootscope, for example
angular.module('myApp')
.service('AdminService', function AdminService($rootScope, $http, StorageService) {
var authtoken = StorageService.get('authtoken');
var clientId = $rootScope.loggedUser.id;
and of course when the check function runs it waits for the response, but the service is being instantiated and $rootScope.loggedUser.id does not exists.
How can I tell the app to wait until the check function receive the response?
This is my code
....
}).run(function($rootScope, $location, AuthService, StorageService) {
var intended = $location.path();
AuthService.check().success(function(data) {
if(data.user) {
$rootScope.loggedUser = data.user;
$location.path(intended);
} else $location.path('login');
});
$rootScope.$on('$routeChangeStart', function() {
if($rootScope.loggedUser) {
....
For example if the user bookmarks the page "myapp.com/#/admin/users", I don't want to redirect to the login, if I have in local storage the authtoken, but this causes the controller to be instantiated, that uses the service, that needs the $rootScope.loggedUser.id that is not yet populated.
And I want to run the function check only when the page (re)loads (not every time the user change route).
I would advise to re-examine your design if you need a service call to check your auth token. Auth tokens typically have expiration time stored in them, so you could just check whether you are within the expiration period without calling the server. You are not compromising security, since auth tokens are signed by the server, and validated when you make server calls to do anything useful. So, under normal circumstances, no separate check call is needed.
But, if you insist, this use case is best handled with the resolve property of the route. This means, however, that every route that cares about the user's logged-in state would have to have a resolve property defined. This does not mean that you have to call the service on each route change.
Instead of using $rootScope.loggedUser to store the user, have it be cached by the AuthService and injected via the resolve parameter.
So, you could do the following:
$routeProvider
.when("some/secure/route", {
controller: "SecureCtrl",
resolve: {
user: AuthService.checkUser
}
});
And in your AuthService:
...
checkUser: function(){
var deferred = $q.defer();
if (cachedUser){
deferred.resolve(cachedUser);
} else {
AuthService.check().success(
function(data){
// cachedUseris defined at AuthService's scope
cachedUser = data.user;
deferred.resolve(data.user);
});
}
return deferred.promise;
}
Then, in your controllers:
.controller("SecureCtrl", function(user){
$scope.userId = user.id;
}
I am looking for a better way to do the transitions between route states in angularjs. Currently I have been following a couple different tutorials to configure angular with a backend api server using authentication on the server side.
http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/
https://vickev.com/#!/article/authentication-in-single-page-applications-node-js-passportjs-angularjs
Both of these describe a similar to identical solution to authentication on server side rather than on the client side. I have a Auth service in angular that uses a method to post to the api server checking whether the user isLoggedIn using an api key.
isLoggedIn: function( event, toState ) {
var user_api = {};
if( "user" in $rootScope ) {
user_api = { "api": $rootScope.user.api };
}
$http.post('http://bac-api/authenticated/', user_api)
.success( function( user ) {
if( toState.url === '/login' && user.authenticated === true) {
$state.transitionTo('dashboard');
}
})
.error( function( user ) {
if( user.authenticated === false ) {
event.preventDefault();
$state.transitionTo('login');
}
});
},
If the api key is not present of course the user is not logged in and the server will send a {"autheticated": false} 401 response. If the user has an api key it uses that api key to check on the server whether it is valid (logged in) or not valid (not logged in).
In order to catch the routes and check whether a user is authenticated I and using "stateChangeStart"
$rootScope.$on("$stateChangeStart", function ( event, toState, toParams, fromState, fromParams ) {
Auth.isLoggedIn( event, toState );
});
The first problem with this is that a state change is not triggered on the intial page load and results in using the routeProvider to transition to the login page. And then throughout the app if a route that is requested is not in the configured routes it will transition to the login page but not trigger a stateChangeStart. So the user could be logged in and sitting at the login page. I would rather it transfer to the dashboard as my configured route.
For the most part this setup seems to be working ok in theory but the routes are choppy. So what will happen is in between checking if the user is logged in the route will start to change and start to show the page but then when the application realizes the user is not logged it will make the switch to the login page. I would like to resolve this choppiness. And instead of getting a glipse of the other pages be able to transfer correctly.
I am using ui-router and states in the application for everything. And then using the Route Provider only to do $routeProvider.otherwise('login') when there isn't a route for the requested route.
I would like to figure out how to stop showing part of new pages (the choppiness) during route transitions while the application is checking for the user being authenticated. Whether that is in a different event that I'm unaware of or whatever.
Also a better way to use routeProvider.otherwise() to trigger a state change and check whether the user isLoggedIn. If logged in transfer to the dashboard ifnot logged in stay on login page until finished logging in.
Sorry if this is so confusing. I am new to angularjs and have been reading everything I can find about routes, states and authentication. This is a new concept for me to learn how to manage authentication on the server side with angular.
-- Edit
After taking the recommendation of MBielski I have implemented resolve into the states to use the Auth.isLoggedIn() service. But this still has not removed the choppiness of switching routes and I even fear that once this is not in local development the choppiness will become worse waiting on the api response. Here is the resolve code for my parent dashboard state.
'dashboard-shell': {
name: 'dashboard-shell',
resolve: {
Auth: function( Auth ) {
return Auth.isLoggedIn();
}
},
views: {
"dashboard-shell": {
controller: 'DashboardController',
templateUrl: 'app/modules/template-manager/partials/dashboard.tpl.html'
}
}
},
Your problem seems to be that every state change results in a POST request that must be handled before you can switch states. I assume you do that to handle session expiry, or because you don't have an authentication cookie to inspect.
You've basically implemented a pessimistic approach to ensure the user is authenticated before doing any other HTTP callbacks.
Another approach might be to use the angular-http-auth HTTP interceptor. This interceptor provides an optimistic approach to ensure the user is authenticated: it just passes every HTTP call on to the backend, but you give it a special callback; when the backend returns a 403 Unauthorized this callback is used to e.g. show a login dialog so the user can (re)authenticate. After that, the interceptor replays all HTTP calls that resulted in a 403.
The ui-router has a resolve option that should clear up the chop. https://github.com/angular-ui/ui-router/wiki. Anything that is contained in the resolve section will be completely resolved before the rest of the route changes happen and the pages load.
isLoggedIn: function( event, toState ) {
var user_api = {};
if( "user" in $rootScope ) {
user_api = { "api": $rootScope.user.api };
}
return $http.post('http://bac-api/authenticated/', user_api);
},
resolve: {
var logged = Auth.isLoggedIn();
logged.then(function(data){
//success
}, function(data){
//failure
});
}