How to: Scope login to subdomain - heartcombo/devise GitHub Wiki
Overview
- Modify the Devise-generated migration to remove the index on email uniqueness constraint
- Change login keys to include
:subdomain - Override Devise hook method
find_for_authentication - Override Devise hook method
send_reset_password_instructions
Modify migration
First, you must remove the email index uniqueness constraint. Because we'll be turning this into a scoped query, we will scope the new index to subdomain. If this is a brand new Devise model, you can open the Devise migration and change the following:
# db/migrate/XXX_devise_create_users.rb
def change
# Remove this line
add_index :users, :email, :unique => true
# Replace with
add_index :users, [:email, :subdomain], :unique => true
end
If this is an existing project, you'll need to create a new migration removing the old index and adding a new one:
rails g migration reindex_users_by_email_and_subdomain
# db/migrate/XXX_reindex_users_by_email_and_subdomain.rb
def up
remove_index :users, :email
add_index :users, [:email, :subdomain], :unique => true
end
def down
remove_index :users, [:email, :subdomain]
add_index :users, :email, :unique => true
end
Change login keys
In your Devise model, add :subdomain to :request_keys. By default :request_keys is set to [].
# app/models/user.rb
class User
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, request_keys: [:subdomain]
end
If you have multiple Devise models and you would like all of them to have the same :request_keys configuration, you can set that globally in config/initializers/devise.rb
config.request_keys = [:subdomain] # default value = []
If additionally you want to still be able to log in using a URL without a subdomain, :request_keys can also take a hash with booleans indicating if the key is required or not.
config.request_keys = { subdomain: false }
If you are using column name other than subdomain to scope login to subdomain, you may have to use authentication_keys. For example, if you have subdomains table and are using subdomain_id on your Devise model to scope User, you will have to add authentication_keys: [:email, :subdomain_id] instead of request_keys: [:subdomain_id] on user.rb. This is because request_keys honors only predefined keys such as :subdomain.
Check that you do not have :validatable in the devise call on the Model
If you do, :validatable will prevent more than one record having the same email, even in different subdomains. If you want to keep some of the validations, you can copy the ones you want from https://github.com/plataformatec/devise/blob/master/lib/devise/models/validatable.rb
Override Devise auth finder hook
For Authenticatable, Devise uses the hook method Model.find_for_authentication. Override it to include your additional query parameters:
# app/models/user.rb
class User < ActiveRecord::Base
def self.find_for_authentication(warden_conditions)
where(:email => warden_conditions[:email], :subdomain => warden_conditions[:subdomain]).first
end
end
Congrats, User login is now scoped to subdomain!
Password recovery with subdomain
By default devise will query the user by email only. This won't work, if you have a user registered with same email for different subdomains.
Overwrite Devise send reset password instructions
# app/models/user.rb
class User < ActiveRecord::Base
def self.send_reset_password_instructions(attributes = {})
# define extra condition to select the right user
recoverable = where(:email => attributes[:email], :subdomain => attributes[:subdomain])
# just copied from Devise::Models::Recoverable#send_reset_password_instructions
recoverable = recoverable.find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
end
Create a custom controller to pass subdomain
To provide the subdomain in the attributes you need to define them in params like:
# app/controllers/users/passwords_controller.rb
class Users::PasswordsController < Devise::PasswordsController
def create
# set host to current request host. this will be passed to send_reset_password_instructions method in User.
params[:user][:subdomain] = request.subdomain
super
end
end
Use custom controller for password resets
Align your routes to use the new controller with something like this:
# config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: {
passwords: "users/passwords"
}
end