Search code examples
ruby-on-railsdeviserails-admincancancan

Rails Admin not authenticating with Cancancan or Devise


I tried this in the config:

  ### Popular gems integration
  config.authenticate_with do
    warden.authenticate! scope: :user
  end
  config.current_user_method(&:current_user)

here, accessing /admin gets me stuck in a login loop. I login then get redirected to the login page.

Previously I had tried

class Ability
  include CanCan::Ability

  def initialize(user)
    # Define abilities for the passed in user here. For example:
    #
      user ||= User.new # guest user (not logged in)
      if user.admin?

with the CanCanCan authentication in rails admin config. This resulted in user always being nil. Even when I put in

config.current_user_method(&:current_user)

How do I fix this to authenticate was an admin?

EDIT: in sessions controller

      if user.admin
        redirect_to rails_admin_url
      else
        render json: user
      end

and I get stuck in the redirect back to signin.

Routes:

Rails.application.routes.draw do
  mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
  devise_for :users, controllers: { sessions: :sessions },
                      path_names: { sign_in: :login }
... then many resources lines

EDIT:

Sessions Controller:

class SessionsController < Devise::SessionsController
  protect_from_forgery with: :null_session

  def create
    user = User.find_by_email(sign_in_params[:email])
    puts sign_in_params
    if user && user.valid_password?(sign_in_params[:password])
      user.generate_auth_token
      user.save
      if user.admin
        redirect_to rails_admin_url
      else
        render json: user
      end
    else
      render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
    end
  end
end

Rails admin config, trying this:

  ### Popular gems integration
  config.authenticate_with do
    redirect_to merchants_path unless current_user.admin?
  end
  config.current_user_method(&:current_user)

In ApplicationController:

def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
  helper_method :current_user

Tried this, current_user is nil.


Solution

  • THE CONTROLLER LOGIC FOR REDIRECT

    The solution from Guillermo Siliceo Trueba will not work with your Users::SessionsController#create action as you are not calling in the parent class create action through the keyword super.

    The method create from the class Devise::SessionsController will run respond_with in the last line using as location the value returned from after_sign_in_path_for(resource)

    class Devise::SessionsController < DeviseController
      # POST /resource/sign_in
      def create
        self.resource = warden.authenticate!(auth_options)
        set_flash_message!(:notice, :signed_in)
        sign_in(resource_name, resource)
        yield resource if block_given?
        respond_with resource, location: after_sign_in_path_for(resource)
      end
    end
    

    I am using this solution in my Users::SessionsController to handle html and json requests and you can implement the same.

    If the controller receives the request with format .json, the code between format.json do .. end is executed, if the request arrives with format.html the parent method is called (I cover all your scenarios).

    Rails router identifies the request format from the .json or .html appended at the end of the url (for example GET https://localhost:3000/users.json)

    class Users::SessionsController < Devise::SessionsController
      def create
        respond_to do |format|
          format.json do 
            self.resource = warden.authenticate!(scope: resource_name, recall: "#{controller_path}#new")
            render status: 200, json: resource
          end
          format.html do 
            super
          end
        end
      end
    end
    

    The html request will be routed to the super create action. You just need to override the method def after_sign_in_path_for(resource) from the parent as I am doing in my ApplicationController.

    if resource.admin then just return rails_admin_url from this method and skip the other lines, otherwise follow the normal behavior and call super#after_sign_in_path_for(resource)

    def after_sign_in_path_for(resource)
      return rails_admin_url if resource.admin
      super
    end
    

    THE ERROR MESSAGES FOR AUTHENTICATION

    warned.authenticate! will save inside self.resource.errors the error messages. You just need to display the errors on your device using json_response[:error] and manipulating the response in your frontend.

    I am rendering them in the SignInScreen Component

    enter image description here

    THE TOKEN AUTHENTICATION

    user.generate_auth_token
    user.save
    

    I use the simple_token_authentication for devise which also allows you to regenerate the token every time the user signs in.

    You just install the gem and add acts_as_token_authenticatable inside your user model.

    class User < ApplicationRecord
       acts_as_token_authenticable
    end
    

    You pass headers X-User-Email and X-User-Token with the corresponding values as explained in their guide.

    Other alternatives to simple_token_authentication