Rails: Using Devise with single table inheritance
Asked Answered
H

4

12

I am having a problem getting Devise to work the way I'd like with single table inheritance.

I have two different types of account organised as follows:

class Account < ActiveRecord::Base
  devise :database_authenticatable, :registerable
end

class User < Account
end

class Company < Account
end

I have the following routes:

devise_for :account, :user, :company

Users register at /user/sign_up and companies register at /company/sign_up. All users log in using a single form at /account/sign_in (Account is the parent class).

However, logging in via this form only seems to authenticate them for the Account scope. Subsequent requests to actions such as /user/edit or /company/edit direct the user to the login screen for the corresponding scope.

How can I get Devise to recognise the account 'type' and authenticate them for the relevant scope?

Any suggestions much appreciated.

Honeyman answered 16/4, 2011 at 23:36 Comment(0)
I
10

I just ran into the exact scenario (with class names changed) as outlined in the question. Here's my solution (Devise 2.2.3, Rails 3.2.13):

in config/routes.rb:

devise_for :accounts, :controllers => { :sessions => 'sessions' }, :skip => :registrations
devise_for :users, :companies, :skip => :sessions

in app/controllers/sessions_controller.rb:

class SessionsController < Devise::SessionsController
    def create
        rtn = super
        sign_in(resource.type.underscore, resource.type.constantize.send(:find, resource.id)) unless resource.type.nil?
        rtn
    end
end

Note: since your Accounts class will still be :registerable the default links in views/devise/shared/_links.erb will try to be emitted, but new_registration_path(Accounts) won't work (we :skip it in the route drawing) and cause an error. You'll have to generate the devise views and manually remove it.

Hat-tip to https://groups.google.com/forum/?fromgroups=#!topic/plataformatec-devise/s4Gg3BjhG0E for pointing me in the right direction.

Idiocrasy answered 27/3, 2013 at 22:27 Comment(4)
I used this and I get the error NoMethodError (undefined method 'type' ... On the line sign_in(resource.type ... What do I need to do to change this?Vestigial
I'm guessing just make a string column in account named type? And set it to the model name?Vestigial
That would be my guess as well. ActiveRecord STI expects that.Idiocrasy
I tried this, and my app is still crashing on the new_registration_path issue mentioned in the note. Since all the Devise views use generic resource paths, how do I manually remove the new_registration_path(Accounts)?Deafen
O
16

There is an easy way to handle STI in the routes.

Let's say you have the following STI models:

def Account < ActiveRecord::Base
# put the devise stuff here
devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable
end

def User < Account
end

def Company < Account

A method that is often overlooked is that you can specify a block in the authenticated method in your routes.rb file:

## config/routes.rb

devise_for :accounts, :skip => :registrations
devise_for :users, :companies, :skip => :sessions

# routes for all users
authenticated :account do
end

# routes only for users
authenticated :user, lambda {|u| u.type == "User"} do
end

# routes only for companies
authenticated :user, lambda {|u| u.type == "Company"} do
end

To get the various helper methods like "current_user" and "authenticate_user!" ("current_account" and "authenticate_account!" are already defined) without having to define a separate method for each (which quickly becomes unmaintainable as more user types are added), you can define dynamic helper methods in your ApplicationController:

## controllers/application_controller.rb
def ApplicationController < ActionController::Base
  %w(User Company).each do |k| 
    define_method "current_#{k.underscore}" do 
        current_account if current_account.is_a?(k.constantize)
    end 

    define_method "authenticate_#{k.underscore}!" do 
    |opts={}| send("current_#{k.underscore}") || not_authorized 
    end 
  end
end

This is how I solved the rails devise STI problem.

Orthoptic answered 25/9, 2013 at 5:22 Comment(1)
Should not :user be :company in this line of code ? ``` # routes only for companies authenticated :user, lambda {|u| u.type == "Company"} do end ```Jingoism
I
10

I just ran into the exact scenario (with class names changed) as outlined in the question. Here's my solution (Devise 2.2.3, Rails 3.2.13):

in config/routes.rb:

devise_for :accounts, :controllers => { :sessions => 'sessions' }, :skip => :registrations
devise_for :users, :companies, :skip => :sessions

in app/controllers/sessions_controller.rb:

class SessionsController < Devise::SessionsController
    def create
        rtn = super
        sign_in(resource.type.underscore, resource.type.constantize.send(:find, resource.id)) unless resource.type.nil?
        rtn
    end
end

Note: since your Accounts class will still be :registerable the default links in views/devise/shared/_links.erb will try to be emitted, but new_registration_path(Accounts) won't work (we :skip it in the route drawing) and cause an error. You'll have to generate the devise views and manually remove it.

Hat-tip to https://groups.google.com/forum/?fromgroups=#!topic/plataformatec-devise/s4Gg3BjhG0E for pointing me in the right direction.

Idiocrasy answered 27/3, 2013 at 22:27 Comment(4)
I used this and I get the error NoMethodError (undefined method 'type' ... On the line sign_in(resource.type ... What do I need to do to change this?Vestigial
I'm guessing just make a string column in account named type? And set it to the model name?Vestigial
That would be my guess as well. ActiveRecord STI expects that.Idiocrasy
I tried this, and my app is still crashing on the new_registration_path issue mentioned in the note. Since all the Devise views use generic resource paths, how do I manually remove the new_registration_path(Accounts)?Deafen
N
1

try to change routes like so:
devise_for :accounts, :users, :companies
because Devise uses plural names for it's resources

Please let me know if it help you

Natty answered 17/4, 2011 at 12:52 Comment(1)
Thanks for your suggestion. Unfortunately, this didn't solve the problem. Logging in via /accounts/sign_in still doesn't allow me to access /users/edit or /companies/edit.Honeyman
S
1

I don't think this is possible without overriding the sessions controller. Each sign_in page has a specific scope that devise will authenticate against as defined by your routes.

It may be possible to use the same sign_in page for multiple user scopes by using the devise_scope function in your routes file to force both :users and :companies to use the same sign in page (a how-to can be found here), but I'm pretty certain that you would have to modify your sessions controller to do some custom logic in order to determine which type of user is signing in.

Solvolysis answered 17/4, 2011 at 20:5 Comment(1)
@Honeyman How were you able to handle this? Have you found a better solution?Iodoform

© 2022 - 2024 — McMap. All rights reserved.