Rails: Using Devise with single table inheritance - ruby-on-rails

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.

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.

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.

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

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.

Related

Before Action Authenticate with Devise and STI not working

I'm trying to use before_action :authenticate_admin! to determine if the current user is an admin or not. I am using devise with single table inheritance (STI).
Here is my User model;
class User < ApplicationRecord
def admin?
false
end
devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable, :lockable
end
And my Admin model;
class Users::Admin < User
def devise_scope
:admin
end
def admin?
true
end
end
Just to test out devise, I have this in my routes file;
authenticated :user, ->(u) { u.admin? } do
resources :teams
end
And indeed, if I am signed in as a different User type (so not Admin), I can't get to that resource.
But this fails to work;
class Teams < ApplicationController
before_action :authenticate_admin!
def index
end
end
I get the error;
undefined method `authenticate_admin!'
Quite new to Rails so I don't understand how to resolve this.
UPDATE
I had to put this in the routes file;
devise_for :admins, class_name: 'Users::Admin'
Previously I only had;
devise_for :users, controllers: {sessions: 'users/sessions'}

STI with Rails and Devise

i'm working on a Ruby on Rails application and using devise to authenticate user.
First of all, i have these models:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :lockable
end
class Admin < User
end
class Partner < User
end
My devise_for on routes:
devise_for :users, :controllers => {
:registrations => "registrations",
:passwords => "passwords"
}
What i want to do? I want do a separate devise for each model, with different views and different strategy, for example: The user login with his registration number and password, the admin and the partner login with email and password (i think that admin and partner devise can be the same, because thier login stratagy is the same).
What is the best way to do this?
Configure routes:
devise_for :users
devise_for :admins
devise_for :partners
Generate devise views
rails generate devise:views user
rails generate devise:views admin
rails generate devise:views partner
Don't forget, add type field in users table for your STI
class AddTypeToUser < ActiveRecord::Migration
def up
add_column :users, :type, :string
end
def down
remove_column :users, :type
end
end

How to add an admin restriction feature through Devise to my rails app?

My rails app has a few cab operators and they have a few cabs, and they are related as follows:
class Operator < ActiveRecord::Base
has_many :cabs
end
I wish to add authentication system so as to create admins for each operator. I am using Devise. Since I need to create path as: operator/:operator_id/admins/sign_up, I generated the Admin model, as:
rails generate devise Admin
Then I modified my routes so as to obtain the above mentioned path:
scope "operators/:operator_id" do
devise_for :admins
end
After a few more modifications, I was able to add new admins and associate them to appropriate operators. However, I want to ensure that an admin only has access to the cabs of the operator to which the admin is associated. Adding the following to the cabs_controller doesn't work:
before_action :authenticate_admin!
as a signed_in admin has access to all other operator's cabs. I want to make sure that:
1. If there is no current_admin, my app asks to sign_in or sign_up
2. If there already is a current__admin signed_in, he/she has access to only the cabs associated to that operator to which the current_admin is assigned.
I am new to Devise. Please advise how I should proceed. Thanks!
Devise is for Authentication, your problem is an Authorization problem which Devise cannot help. You can look into Cancan https://github.com/ryanb/cancan :)
You don't need to scope your devise for admins. I'm not sure if it would solve any issue with access. It also makes your code harder to maintain.
I would advise you to focus on relationships instead
class Operator < ActiveRecord::Base
has_many :cabs
has_many :admins
end
class Operator < ActiveRecord::Base
has_many :cabs, through: operator
belongs_to :operator
end
Normally the way I handle such cases later is to write special in the cab model :
def is_admin?(admin)
admin.cabs.include?(#cab)
# you can put more logic here if you have different levels of access
end
and later in the controller when you want to restrict access:
if #cab.is_admin?(current_admin)
# do the stuff
else
# go away message
end
or for instance if you list cabs in index:
def index
current_admin.cabs.page(params[:page]) # this is assuming you're using pagination with something like Kaminari
end
current_admin from divise, you may want to also run :authenticate_admin! before filter to make sure you have you admin and it's not nil
Of course you can put it in price message and run it as before filter.
The most simple solution is to use STI and have Admin < User, then a couple of
devise_for :users
devise_for :admins
If you do not want to use STI, some custom overridings may do the trick, if you add a role method to your users table for exemple.
#routes
devise_for :users
namespace :admin do
devise_for :admins,
singular: :admin,
plural: :admins,
class_name: 'User',
only: [:sessions],
path: '/',
module: :devise
end
#lib/overrides/devise
require "devise/strategies/authenticatable"
require "devise/strategies/database_authenticatable"
module Devise
module Strategies
class DatabaseAuthenticatable
def authentication_hash
if mapping.name == :admin
#authentication_hash.update(role: :admin) #will add a where(role: :admin) clause
else
#authentication_hash
end
end
end
end
end
class Admin::ApplicationController < ApplicationController
skip_before_filter :authenticate_user!
before_filter :authenticate_admin!
end
You can use role base Authentication.
Generate User model
class User < ActiveRecord::Base
enum role: [:operator, :admin]
after_initialize :set_default_role, :if => :new_record?
def set_default_role
self.role ||= :operator
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
generate migration to add role
rails g AddRoleToUser role:integer
Please take a look ENUM

Multiple devise user type root routes

I have just set up my STI structure with a Devise User model. I have two user types (business and lender) and would like them to be sent to separate root paths after signing in. My models look like this:
user.rb
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
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :type
end
lender.rb
Class Lender < User
end
business.rb
Class Business < User
end
My routes file looks like:
Lendingloop::Application.routes.draw do
# User route
devise_for :users, skip: [:registrations]
# Lending routes
devise_for :lenders, skip: :sessions
authenticated :lender do
root :to => "LenderAccount#dashboard", :as => "lender_authenticated_root"
end
#Business routes
devise_for :businesses, skip: :sessions
authenticated :business do
root :to => "BusinessAccount#dashboard", :as => "business_authenticated_root"
end
# Error Routes
get "/404", to: 'errors#not_found'
get "/422", to: 'errors#unacceptable'
get "/500", to: 'errors#internal_error'
# Root route
root :to => 'StaticPages#landing'
end
As you can see I have an authenticated do block which I would like to redirect lenders and businesses to their specific root pages. I have a LenderAccount and BusinessAccount controller setup with dashboard actions in them and corresponding views.
When I log in as a lender or a business, I'm redirected to my root_path which is 'StaticPages#landing'. That should only be for non-logged in users.
Update
I went through the devise documentation and added the following to my application_controller.rb file:
def after_sign_in_path_for(user)
if user.type == "Business"
business_authenticated_root_path
elsif user.type == "Lender"
lender_authenticated_root_path
else
root_path
end
end
I am also seeing a strange error in my console when I attempt to go to the dashboard page directly from lender_authenticated_root_path:
Rendered /Users/questifer/.rvm/gems/ruby-1.9.2-p320#rails3tutorial/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/routing_error.erb within rescues/layout (0.7ms)
[2014-04-22 12:31:31] ERROR Errno::ECONNRESET: Connection reset by peer
/Users/bvlaar/.rvm/rubies/ruby-1.9.2-p320/lib/ruby/1.9.1/webrick/httpserver.rb:56:in `eof?'
/Users/bvlaar/.rvm/rubies/ruby-1.9.2-p320/lib/ruby/1.9.1/webrick/httpserver.rb:56:in `run'
/Users/bvlaar/.rvm/rubies/ruby-1.9.2-p320/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
Does anyone have an idea as to how I can get these authenticated blocks to handle the proper root redirects?
I got the roots working by changing my routes.rb file to:
routes.rb
root :to => 'business_account#dashboard', :constraints => lambda { |request| request.env['warden'].user.class.name == 'Business' }, :as => "business_root"
root :to => 'lender_account#dashboard', :constraints => lambda { |request| request.env['warden'].user.class.name == 'Lender' }, :as => "lender_root"
Now my two user types can login to their accounts and be directed to their respective controllers and dashboards.
I think I can help. I had a similar problem. In my routes.rb file I created a "home" screen for non-logged in users.
Rails.application.routes.draw do
root "home#index"
devise_for :users
resources :home
resources :dashboard
The home_controller.rb looks like this:
class HomeController < ApplicationController
def index
if user_signed_in?
redirect_to :controller=>'dashboard', :action =>'index'
end
end
end
dashboard_controller.rb looks like this, and is only for logged in users:
class DashboardController < ApplicationController
before_filter :authenticate_user!
def index
end
end
The views for each (home.html.erb & dashboard.html.erb) can then reflect content for visitors or logged in users.
I used this tutorial to sort it out. I don't know if it's overkill or not, this is my first application, but ti seems to be what you are asking for.
http://www.tonylea.com/2012/rails-authentication-with-devise/
Hope that helps.

Devise with Single Table Inheritance (STI)

I am working on Mongoid and Rails4
I have model structure (STI) like this,
Class User
devise :database_authenticatable, :registerable
end
User Class is having devise functionality
class Teacher < User
end
class Student < User
end
In route.rb,
devise_for :users
devise_for :teachers, :skip => :sessions
devise_for :students, :skip => :sessions
In a student_profile controller i have defined,
before_filter :authenticate_student!
After signed in as "student" if i clicks on student_profiles_path its redirecting to root file. In database user type is storing as "type" : "Student", while sign up i am passing the _type value via collectionselect.
Is there any setting or code stuff i have to do...!!!

Resources