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.


ruby 2.0.0-p0
gem 'rails', '4.0.0.beta1'
gem 'devise', git: "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.


Given you've a similar situation as below:

class User < ActiveRecord::Base

class Admin < User

class Seller < User

class Buyer < User

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


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]
  namespace :seller do
    root to: "home#index"
    resources :profile, only: [:edit, :update]
  namespace :buyer do
    root to: "home#index"
    resources :profile, only: [:edit, :update]

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

Doing this necessitates overriding the Devise sessions controller


class SessionsController < Devise::SessionsController

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


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?
    when "Admin"
    when "Buyer"
    when "seller"
    end if user_signed_in?

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


    def current_admin
      @current_admin ||= current_user if user_signed_in? and == "Admin"

    def current_buyer
      @current_buyer ||= current_user if user_signed_in? and == "Buyer"

    def current_seller
      @seller_seller ||= current_user if user_signed_in? and == "Seller"

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

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

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

    def require_admin

    def require_Buyer

    def require_seller

    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

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)
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