I am having serios problems with implementing login system in my angularJS SPA. This is the hardest part in whole application development as every other aspect I already have handled. I am having trouble to come up with correct service design (I thought that I had understand promises but seems that not as good just yet).
The only authorization method for app will be FB login coupled with native server side logic. On FB JS SDK login success app will make http post to server where I use FB PHP SDK to get access token from fb cookie, get fb id and check if we there is user with that fb id in database. If there is - return all user credentials to app, if not - return flag for registration.
For registration I need only one thing that FB does not provide - user will place one marker on map and coordinates will be sent to server and saved in database.
mysql> DESCRIBE users; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | fbid | ... | NO | | NULL | | | lat | ... | NO | | NULL | | | lng | ... | NO | | NULL | | | name | ... | NO | | NULL | | | ... | ... | NO | | NULL | | +---------+------------+------+-----+---------+----------------+
I will use fbid to authorize(recognize) user, my own id to work inside app(client and server side), coordinates comes from register and everything else from FB.
There are 4 ways that I could login user from app:
- Autologin. On app run I check fb login statuss and if
status === connected
then I automatically log user in. - On user action. When user click on "login" btn.
- If user tries to access route that requires login. The same method that is used on user action is called and if it return success then user can continue to protected route, if not - transition is canceled
- If server return 401 statuss. If session expires same method should be envoked and on succesful re-login request should be made again.
How I see it, every case rely on one method, lets say AuthService.login
:
- Autologin calls
AuthService.login
fromapp.run()
method if FB statuss isconnected
- On user click
AuthService.login
is called - I add
transitionHookFn
foronStart
lifecycle of transition and run AuthService.login if login is required and user is not logged in. On successful login I continue, otherwise - cancel transition(I use ui-router) - I use
$httpProvider.interceptor
to callAuthService.login
$http response withstatuss 401
. On succesfull login I request same request again, otherwise - let requst fail(and handle route logic so that failed request would cancel transition)
My core question lies in AuthService.login
. How that method should be structured so that I could reuse it in all four cases?
What I have so far:
I use ui-router and this for integrating FB JS SDK in angular. For handling user statuss in app I have service:
(function(){ angular.module('userService') .factory('UserService', userService); userService.$inject = []; function userService(){ var user = { isLogged: false, info: '' }; return { setUser: setUser, getUser: getUser, clearUser: clearUser }; function setUser(info){ user.isLogged = true; user.info = info; } function getUser(){ return user; } function clearUser(){ user.isLogged = false; user.info = {}; } } })();
I inject it where ever I need to know user statuss and use userService.getUser
.
For authorization I have service:
(function(){ angular.module('authService') .factory('AuthService', AuthService); AuthService.$inject = ['UserService', 'Facebook', '$http', 'baseUrl', '$q']; function AuthService(UserService, Facebook, $http, baseUrl, $q){ return { autoLogin: autoLogin, login: login }; function autoLogin(){ Facebook.getLoginStatus(function(response) { statusChangeCallback(response); }); } function login(){ Facebook.login(function(response) { statusChangeCallback(response); }); } function statusChangeCallback(response) { console.log('statusChangeCallback'); if (response.status === 'connected') { $http({ method: 'POST', url: /*login endpoint*/ }).then(/*Dont know what should do here*/) } } } })();
At this point login endpoint return JSON {user: null, register: true}
if it is first time login and {user: {/*data*/}, register: false}
but I am open for changes there. I have tried several ways handling the response but I always run into some trouble in one of four cases described earlier. Basically if register: false
then I should use userService.setUser(response.data.user)
, if register:true
- $state.target('register')
for case 3 I have:
$transition.onStart({ to: function(state) { return state.data != null && state.data.authRequired === true; }},function(trans){ var AuthService = trans.injector().get('AuthService'); /*Should run AuthService.login and: 1.continue if completely logged in 2.$state.target('register') if register needed 3.cancel otherwise*/ });
for case 4 I have:
var sessionRecoverer = { responseError: function(response) { if (response.status == 401){ var AuthService = $injector.get('AuthService'); var $http = $injector.get('$http'); var deferred = $q.defer(); /*again call login and redirect if register or do http request again if login success*/ } return $q.reject(response); //Think that this would be correct to reject any other error } };
To sum up - I understand how $http interceptor and ui-router transition works. I dont undestand how to deal with promise from AuthService as there are two different outcomes from server response and I have hard time understanding how to keep UserService.setUser method inside AuthService login flow. First two cases (autologin and login on user demand) are easy - AuthService need to set user info or redirect to register. Problems arise with last two option as in both of them some other service needs to determine whether to continue or not.
UPDATE: Just an idea in my mind - coulnt I return custom statuss code or header if register is required, catch it with http interceptor and redirect + fail request. Then I would have simple mechanism if AuthService returns promise consider login success otherwise fail. ?
1 Answers
Answers 1
I had a similar authentication/authorization workflow for an AngularJS project I was working on. I wasn't using PHP or Facebook IDP, instead I was using ASP.NET Web API and Microsoft Azure B2C as the IDP. However, let's look at this from the AngularJS perspective and be agnostic towards the IDP and server-side technology.
The simplest flow would involve having the root (home page) of the application be a non-authenticated page. The user would click a 'Login' button which would initiate your authentication process. This would make a call to your AuthService.login()
function. This call should be made from a controller and should be wrapped in a promise to avoid using callbacks. (Note: You could totally have your auto-login logic in the .run()
block instead of doing the button click or do both as it seems you'd like to do -- the logic would be the same in your .run()
block).
function login(){ var deferred = this.$q.defer(); Facebook.login(function(response) { deferred.resolve(response); }); return deferred.promise; }
Then, from your controller that calls AuthService.login()
, you need to handle the POST request to your API to update/get the user information. That would look something like this (note I refactored your HTTP call into a new initUserData()
function):
AuthService.login().then(function(response) { if (response.status === 'connected') { AuthService.initUserData().then(function(userObject) { if(userObject.data.register === true) { $state.go('register'); } else { userService.setUser(response.data.user); $state.go('whereverUserShouldGoInThisCase'); } }); } });
Good, so far we've covered cases #1 and #2. Let's take a look at #3. If we don't want users to hit a route that requires authentication, we can set up a hook using the UI-Router in the application's .run()
block.
First, though, we need to declare which routes require being authenticated.
.state('home', { url: 'home', restricted: true })
We can do this by adding a new property of any name (in this case we use restricted
) to all of the routes you want restricted (via authentication) in your application.
Then in the .run()
block we hook into the UI-Router via $stateChangeStart
. This will be called anytime you transition to/from states (at the beginning of the transition) including abstract states.
$rootScope.$on('$stateChangeStart', (event, next, nextParams, current, currentParams), function() { // Any time the user tries to hit a restricted route, we need to check if they're logged in if (next.restricted) { AuthService.login().then(function(response) { if(response.status !== 'connected') { event.preventDefault(); // Prevent going to the transitioning state $state.go('login'); // Go to the state we want to send the user to } }); } });
This should cover #3, let's look at #4. You certainly have the right idea of using an HTTP Interceptor. Here we can duplicate the same logic as before to handle where the user is redirected on 401.
responseError: function(response) { if (response.status == 401){ var AuthService = $injector.get('AuthService'); AuthService.login().then(function(response) { if(response.status === 'connected') { AuthService.initUserData().then(function(userObject) { if(userObject.data.register === true) { $injector.get('$state').go('register'); } else { userService.setUser(response.data.user); $injector.get('$state').go('whereverUserShouldGoInThisCase'); } }); } }); } return $q.reject(response); // Yes, this will properly reject your response }
Some final notes:
- I would consider using Local Storage to persist user information instead of storing it in service variables. This will allow the information to persist even when the page is hard refreshed.
- Having the registration flow/normal flow makes everything a bit more difficult. I had the same troubles in my application doing this. You need to be very careful as to how you direct the flow in your application from these different "roles".
- You certainly have the right idea using an HTTP interceptor for #4, however for #3 make sure you use
$stateChangeStart
. This hook is incredibly powerful, and you can tap into the previous/next routes which really helps for routing logic in your application when you have different "roles" (i.e. registered/non-registered users). - I'm not sure if your Facebook SDK has an error callback for login, but I assume it would have one. Make sure you implement the error callback and do a
deferred.reject()
when the error occurs. - Finally, my goal here wasn't to write your code for you. I'm sure you'll have to add quite a bit of logic to handle your application's exact use cases. But, it looks like for the most part you had the right idea, and I think you'll be able to implement your logic from here.
0 comments:
Post a Comment