Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

I have a rails app using devise with 2 different models corresponding to vastly different roles (no STI - different models altogether).

I am planning to move to frontend to AngularJS. Want to know whats the best way to structure the app.

Here are my initial thoughts:

  1. Each user model has a different base logged in page (domain.com/user And domain.com/company)
  2. Before rendering that page I check if the correct role has signed in, using devise, and if not redirect to corresponding login page (server side redirect)
  3. All non-logged in pages are non-Angular for this to happen effectively.
  4. Logged in pages are all Angular-ified

This ensures that I don't have to worry about making angular and devise work well together. In a module I can be sure of having a logged in user(or company)

Please suggest if this sounds good.

Also, I would ideally prefer if there is (not too complicated) way in which entire app can use Angular. (login redirect to corresponding login pages etc.). I have seen some examples of Angular + Devise in form of service modules etc. but almost all of them seem like they will work for one model only.

EDIT: I would ideally like to be able to use as much of Devise's functionality as possible.

share|improve this question
add comment

2 Answers

I would suggest that you rethink your problem pretending that devise and angular have nothing to do with each other.

Let's assume that you're not going to render any html templates in rails. You're just going to use rails to create a json api http://railscasts.com/episodes/350-rest-api-versioning?view=asciicast, and the interface for the app is going to be a single page angular app. UI routing will all be done from within your angular app.

  1. You create a sign in route in your angular app, it has a form pointing to your server's devise sign-in route /api/v1/user/sign-in.
  2. When a user hits submit on the form angular can do some front end validation and send the data to your server.
  3. In your rails controller devise will authenticate who this person is, after devise authenticates them your server route should send back the user's information as a json response (or the error messages).

The angular service which made the request to your devise endpoint will now have access to the user's information (from the server's json response). It can check properties on this user object like their role, and then it can use angular's location service to change the route to wherever this role should go.

When the route changes angular will render the route's view and everything will continue on its merry way.

If you need to change the UI within a route based on the role see this question RESTful Authorization in the UI

When the user comes back to your site later you can make the angular app detect that they're still logged in. The angular service that you wrote to to sign a user in can also be responsible for attempting to get the current user, and if there is none, redirect to the sign in page.

Devise gives you a helper called current_user which will give you access to an instance of the user that is logged in. You should create a server route like /api/v1/current_user and have it return the users information. If the user is logged in, redirect to their home page just like after a sign in. Otherwise redirect to the sign in page.

If you don't know angular very well I'd suggest that you spend some time on this site https://egghead.io/

Also you may need to to implement authorization logic on the server http://railscasts.com/episodes/192-authorization-with-cancan

share|improve this answer
    
So as I mentioned I have two different models instead of two different roles for one model. So I have to do stuff like => when a user tries to access a given url, figure out which role it requires, redirect to corresponding login page. Also, I have two different login. To give some context, I have a decent sized non-Angular Rails app that we are moving to Angular. –  nik-v Jun 4 at 15:49
    
In your /api/v1/current_user endpoint you could return current_user || current_company similar to this question stackoverflow.com/questions/10549038/… You don't need to have a role property on the json it returns. As long as your angular code can determine the type of the model from the json than it can use the location service to redirect to the correct rotue. Stack Overflow probably isn't your best bet for a broad architectural discussion, maybe try a mailing list groups.google.com/forum/#!forum/rubyonrails-talk –  The Q CS or GS Jun 4 at 18:10
    
Thanks for the tip. Posted there as well. I think I broadly agree with the approach you suggested, its just that setting up devise initially required me to do almost nothing to fire it up, so I was looking for essentially an option wherein I could leverage as much functionality of Devise as possible. What you suggest is sort of "from the basics" which is obviously correct, but slightly more involved. –  nik-v Jun 4 at 22:57
add comment

Building a two headed Auth Monster

So what you are describing is possible, but it will definitely be a pain. You will have to build up a bunch of code to determine if a user is a NormalUser vs AdminUser in the Rails side. In the Angular side you will have to determine which login a user needs, NormalUser vs AdminUser. That being said, it is possible and here are some pointers that should help.

Docs are your friend

You really really really want to have a good understanding of how Devise works. Devise does a lot of magic under the covers.

For auth to work with Angular, you will need to be familiar with $httpProvider.interceptors, Services, and Resources.

Rails

Routes

Example routes for having two models in Devise:

devise_for :normal_users

devise_for :admin_users

# route for the sample controller below
resource :users do
  collection do
    get :current
  end
end

What Devise brings to the table

Devise will create helper methods in your controllers along the lines of:

current_normal_user

authenticate_normal_user!

current_admin_user

authenticate_admin_user!

So you would have to check both to see if a sessions has been authenticated, best plan is to to wrap both checks into a custom before_action, something along the lines of before_action :authenticate_all_users.

Example Controller for Users

Here is an example UsersControllers that returns JSON Angular will use to check for authentication.

class UsersController < ApplicationController
  # Needs a before_action that authenticates the user

  respond_to :json

  def current
    @user = current_normal_user || current_admin_user

    respond_with @user
  end

end

Angular

The Safety Net

I found this interceptor very useful when handling auth in Angular. Add this to your app.config when setting up Angular, it redirects to a specified page if 401 status code is returned by any http request. This is simpler than having all Resources forced to handle the possibility that a user is not authenticated. (as coffeescipt)

# Monitors the requests and responses of angular
#  * Redirects to /users/sign_in if status is a 401
app.config [
  "$httpProvider"
  ($httpProvider) ->
    $httpProvider.interceptors.push ($q) ->
      request: (config) ->
        config or $q.when(config)

      requestError: (rejection) ->
        $q.reject rejection

      response: (response) ->
        response or $q.when(response)

      responseError: (rejection) ->

        # Not logged in, redirect to login
        if rejection.status is 401
          $q.reject rejection

           # Change this with desired page
          window.location = "/users/sign_in"

        else
          $q.reject rejection

In your scenario, you will probably need to add logic to determine which login page they should see, normal_user vs admin_user.

User Info

Last is an example Angular service that grabs the user information. If the user is not authenticated, the 401 status will be caught by the previous $httpProvider and the user will be dealt with accordingly. Otherwise, if the user is authenticated, the user's information will populated to the $rootScope.currentUser. You simply have to add this as a dependency in a controller that should be auth protected. (in coffeescript)

angular.module("TheAngularApp.services").service "CurrentUserService", ($rootScope, $http, CurrentUser, User) ->
    userService =
        reloadCurrentUser: ->
            @currentUser = CurrentUser.show((user) ->
                # set in scope
                $rootScope.currentUser = user
            )
            @currentUser

    userService.reloadCurrentUser()
    userService

This service depends on the CurrentUser Resource that points at /users/current.json endpoint. In your scenario with the CurrentUserService, possible options for dealing with multiple models are:

  • Check twice if the user is authenticated, once for normal_user and again to admin_user
  • Create a custom controller method that checks the auth of normal_user and admin_user, this is roughly what the sample rails controller is doing.

Extra Credit

Optionally, I would look into ng-idle as a way to allow sessions to expire within Angular.


Update from Comments

My logged in page starts displaying while the backend request for user info is happening, and consequently the redirect to login page is quite visible and leads to a bad ux.

The easiest way is to use a non-angular page for the login, such as the one provided by Devise. Once a user successfully logs in, the page they are directed to loads up the Angular App. This way you can always assume a user is logged in when they are using Angular.

If this is not an possible, you will have to make the auth request from Angular. This means you will need to have the user wait until you can check the promise from the auth request. Once it is valid, you move the user to the correct route.

WARNING I believe Devise sends a status code of 401 if you fail to log in, which will trip the interceptor previous discussed. To get around this, the interceptor will have to exclude paths that handle the auth requests.

Example Resource for handling Auth:

Session.create {email: email, password: password}, success = (user) ->
   # User was successfully authenticated
   $scope.currentUser = user
   $location.path( "/" );  
, error = (data, status, headers, config) ->
   # Failed to auth, notify user
)
share|improve this answer
    
Not the exact answer, but comes closest to what I want, given the question was very open ended. –  nik-v Jun 11 at 14:12
    
Interceptor info proving very helpful as I implement this. One question. My logged in page starts displaying while the backend request for user info is happening, and consequently the redirect to login page is quite visible and leads to a bad ux. Wanted to know if there is a good general practice on when to start displaying the page, so as to prevent such issues –  nik-v 2 days ago
    
The easiest way is to use a non-angular page for the login, such as the one provided by Devise. Once a user successfully logs in, the page they are directed to loads up the Angular App. Hrm, let me expand above. . . –  mguymon 2 days ago
    
already gave that a lot of thought. The problem there is that its problematic to share internal links, as then i can't redirect back to them after login since rails doesn't get the part after #, and I will be re-bootstrapping angular module after signin. –  nik-v 2 days ago
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.