AngularJS: Use devise with multiple models

前端 未结 2 1815
悲哀的现实
悲哀的现实 2021-01-14 15:44

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 fron

2条回答
  •  -上瘾入骨i
    2021-01-14 16:23

    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
    )
    

提交回复
热议问题