Allow Users to Login with Username or Email Using Devise
Devise is a flexible authentication solution for Rails based on Warden, I love it because it's very simple to use and easy to extend or override. Many people would say that It's too huge to include all the devise's modules, but what I would say is that, take it easy, agile is the most important, and it's not so huge because of most of the modules are necessary for such a simple project.
Many websites are both enabled username and email to sign in, It will be very friendly to the users, people will unhappy that every time they must type such a long email address. So Let me show you how to allow users to login with username or email using Devise.
First of all, you must have a column named 'username' in your users table.
Then we can start, you should modify the User model and add username to attr_accessible.
attr_accessible :username
To support both username and email, you should create a login virtual attribute in Users and set the accessible
# Virtual attribute for authenticating by either username or email
# This is in addition to a real persisted field like 'username'
attr_accessor :login
attr_accessible :login
As default, Devise use :email for authentication in the authentication_keys, so you should tell Devise to use
:login in the authentication_keys, modify config/initializers/devise.rb to have:
config.authentication_keys = [ :login ]
You should overwrite Devise’s find_for_database_authentication method in user model
# This is for ActiveRecord
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
login = conditions.delete(:login)
where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.strip.downcase }]).first
end
# This is for Mongoid:
field :email
def self.find_for_database_authentication(conditions)
login = conditions.delete(:login)
self.any_of({ :username => login }, { :email => login }).first
end
Note: This code for Mongoid does some small things differently then the ActiveRecord code above. Would be great if someone could port the complete functionality of the ActiveRecord code over to Mongoid [basically you need to port the ‘where(conditions)’]. It is not required but will allow greater flexibility.
After these settings, make sure you have the Devise views in your project so that you can update devise/sessions/new view.
# devise/sessions/new.html.erb
- <p><%= f.label :email %><br />
- <%= f.email_field :email %></p>
+ <p><%= f.label :login %><br />
+ <%= f.text_field :login %></p>
Add i18n translation for login field
en:
activerecord:
attributes:
user:
login: "Username or email"
Yeah, you have done with the configuration to allow user sign in with username or email address, now there is another problem, you also need to allow users to recover their password using either username or email address, right? Let's do it.
Devise set the default reset password keys to :email, so you have to tell Devise to use :loign
in the reset_password_keys
# config/initializers/devise.rb
config.reset_password_keys = [:login]
Devises's finder methods in user model should be overwritten to support both username and email address.
This is for ActiveRecord
protected
# Attempt to find a user by it's email. If a record is found, send new
# password instructions to it. If not user is found, returns a new user
# with an email not found error.
def self.send_reset_password_instructions(attributes={})
recoverable = find_recoverable_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
def self.find_recoverable_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
(case_insensitive_keys || []).each { |k| attributes[k].try(:downcase!) }
attributes = attributes.slice(*required_attributes)
attributes.delete_if { |key, value| value.blank? }
if attributes.size == required_attributes.size
if attributes.has_key?(:login)
login = attributes.delete(:login)
record = find_record(login)
else
record = where(attributes).first
end
end
unless record
record = new
required_attributes.each do |key|
value = attributes[key]
record.send("#{key}=", value)
record.errors.add(key, value.present? ? error : :blank)
end
end
record
end
def self.find_record(login)
where(["username = :value OR email = :value", { :value => login }]).first
end
These two are all for Mongoid
# as finder
def self.find_record(login)
found = where(:username => login).to_a
found = where(:email => login).to_a if found.empty?
found
end
# This can be optimized using a custom javascript function
def self.find_record(login)
where("function() {return this.username == '#{login}' || this.email == '#{login}'}")
end
Don't forget to update your view and enjoy
