STI with Rails 4.0 beta and Devise

Two weeks of consecutive articles? Nice, I'm actually sticking with this blog for once. Now lets get to the business at hand.

I recently setup Devise with Rails 4.0 and I wanted to show the interwebs how I accomplished while using single table inheritance in my user models. It was surprisingly easier then I thought it would be. Rails 4 has improved their STI support, so a good amount of custom code needed is not necessary anymore.

Gemfile

ruby 2.0.0-p0
gem 'rails', '4.0.0.beta1'
......
gem 'devise', git: "git://github.com/plataformatec/devise.git", branch: "rails4"
gem 'protected_attributes'

We are using the Rails 4 branch from the Devise repo and the protected attributes gem to avoid overriding the default Devise controllers. Of course normally this is not a dangerous thing to do but I preferred to not have to write a bunch of custom routes and keep my routes simpler. Also, we can still use the strong parameters with our other models which I'm actually liking, only our User model will have some attr_accessible code. But, if you so choose to not use protected attributes gem, I will show you you how to override the necessary Devise functions but most likely they will have updated their code to not use protected attributes.

Models

Given you've a similar situation as below:

class User < ActiveRecord::Base
end

class Admin < User
end

class Seller < User
end

class Buyer < User
end

run the devise generator, note: this will fail without the necassary Devise rails4 branch.

rails g devise:install

Your User.rb model should be like so now:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable

  attr_accessible :name, :email, :password, :password_confirmation
end

Routes

This is the most important part and using the default Devise helpers we can accomplish what we want. Note how I've namespaced the user scopes. Feel free to customize to your needs, but you should have something similar to the below.

  devise_for :users, skip: [:registrations]
  devise_for :admins, skip: [:sessions, :registrations]
  devise_for :sellers, skip: :sessions
  devise_for :buyers, skip: :sessions

  namespace :admin do
    root to: "home#index"
    resources :sellers
    resources :profile, only: [:edit, :update]
  end
  namespace :seller do
    root to: "home#index"
    resources :profile, only: [:edit, :update]
  end
  namespace :buyer do
    root to: "home#index"
    resources :profile, only: [:edit, :update]
  end

Of course you do not need to do namespace your user subclasses, it just makes more sense to me and gives me a good base to keep my views more OO and organized down the road.

Now in my situation, only sellers and buyers will be able to register but all of our subclasses need links to be generated to the Devise controllers but we want to keep the session pass scoped to Users only and not generate routes for a users registrations In the past to customize the session paths "login, logout etc" you'd pass a block to the devisefor helper but this practice has been depreciated so if you still want to override those paths, you must use the devisescope helper.

For example:

devise_for :users do
  delete 'logout', to 'sessions#destroy', as :destroy_user_session
  get 'login', to: 'sessions#new', as: :new_user_session
  put 'login', to: 'sessions#create', as: :user_session
end

Doing this necessitates overriding the Devise sessions controller

'app/controllers/sessions_controller.rb

class SessionsController < Devise::SessionsController
end

Now, with our routes in place and our models setup. We're actually good, just need those default devise views

rails g devise:views

Caveat, some of the links in the views/devise/shared/_links.erb partial will throw exceptions so you'll either want to customize them or remove

Helpers

Devise gives you some default helpers to use in your controllers and views, usersignedin?, currentuser, and even currentadmin but current admin requires the base devise db model to actually be a Admin base so we'll want to override this becuase of our STI situation.

Here is an example of my application_controller.rb file

class ApplicationController < ActionController::Base

  protect_from_forgery with: :exception
  helper_method :current_admin, :current_seller, :current_buyer
                :require_admin!, :require_seller!, :require_buyer!

  def account_url
    return new_user_session_url unless user_signed_in?
    case current_user.class.name
    when "Admin"
      admin_root_url
    when "Buyer"
      carrier_root_url
    when "seller"
      seller_root_url
    else
      root_url
    end if user_signed_in?
  end

  def after_sign_in_path_for(resource)
    stored_location_for(resource) || account_url
  end

  private

    def current_admin
      @current_admin ||= current_user if user_signed_in? and current_user.class.name == "Admin"
    end

    def current_buyer
      @current_buyer ||= current_user if user_signed_in? and current_user.class.name == "Buyer"
    end

    def current_seller
      @seller_seller ||= current_user if user_signed_in? and current_user.class.name == "Seller"
    end

    def buyer_logged_in?
      @Buyer_logged_in ||= user_signed_in? and current_buyer
    end

    def admin_logged_in?
      @admin_logged_in ||= user_signed_in? and current_admin
    end

    def seller_logged_in?
      @seller_logged_in ||= user_signed_in? and current_seller
    end

    def require_admin
      require_user_type(:admin)
    end

    def require_Buyer
      require_user_type(:buyer)
    end

    def require_seller
      require_user_type(:seller)
    end

    def require_user_type(user_type)
      if (user_type == :admin and !admin_logged_in?) or
        (user_type == :seller and !seller_logged_in?) or
        (user_type == :buyer and !buyer_logged_in?)
        redirect_to root_path, status: 301, notice: "You must be logged in a#{'n' if user_type == :admin} #{user_type} to access this content"
        return false
      end
    end
end

Of course, I'll eventually want to move this into a controller concern to clean up the application controller. And notice how I'm using different "authenticateuser!" methods, I find it convenient to scope buy the subclasses in my controllers. And not how I've overridden the devise aftersignoutfor(resource)

Departing tidbits

The last techinique I wanted to show you is what I promised earlier where we wouldn't use the protected_attributes gem. Just override the devise/registrations and devise/passwords controller and add the below method to it.

#registrations controller example
def resource_params
  params.require(resource_name).permit(:email, :password, :password_confirmation)
end
private :resource_params

Make sure to specify your controller routes in the routes.rb file

devise_for :users, controllers: {
  registrations: "my_devise/registrations",
  passwords: "my_devise/passwords"
}

I look forward to your thoughts and suggestions on how to improve my example.

Harmony be onto you

Make sure to specify