How To: Allow users to sign in using their username or email address
For this example, we will assume your model is called User
Create a username field in the users
table
- Create a migration:
rails generate migration add_username_to_users username:string
- Run the migration:
rake db:migrate
- Modify the
User
model and add username to attr_accessibleattr_accessible :username
Create a login virtual attribute in Users
- Add login as an attr_accessor
# Virtual attribute for authenticating by either username or email # This is in addition to a real persisted field like 'username' attr_accessor :login
- Add login to attr_accessible
attr_accessible :login
Tell Devise to use :login in the authentication_keys
- Modify config/initializers/devise.rb to have:
config.authentication_keys = [ :login ]
- If you are using multiple models with Devise, it is best to set the authentication_keys on the model itself if the keys may differ:
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :authentication_keys => [:login]
- Overwrite Devise’s find_for_database_authentication method in Users model
- 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
- For Mongoid:
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.
field :email def self.find_for_database_authentication(conditions) login = conditions.delete(:login) self.any_of({ :username => login }, { :email => login }).first end
- For MongoMapper:
def self.find_for_database_authentication(conditions) login = conditions.delete(:login).downcase where('$or' => [{:username => login}, {:email => login}]).first end
Update your views
- Make sure you have the Devise views in your project so that you can customize them
Rails 3:
rails g devise:views
script/generate devise_views
- Modify the views
- sessions/new.html.erb:
- <p><%= f.label :email %><br /> - <%= f.email_field :email %></p> + <p><%= f.label :login %><br /> + <%= f.text_field :login %></p>
- registrations/new.html.erb
+ <p><%= f.label :username %><br /> + <%= f.text_field :username %></p> <p><%= f.label :email %><br /> <%= f.email_field :email %></p>
- registrations/edit.html.erb
+ <p><%= f.label :username %><br /> + <%= f.text_field :username %></p> <p><%= f.label :email %><br /> <%= f.email_field :email %></p>
- sessions/new.html.erb:
Manipulate the :login label that Rails will display
- Modify config/locales/en.yml to contain something like:
Rails 2:
activemodel: attributes: user: login: "Username or email"
en: activerecord: attributes: user: login: "Username or email"
Allow users to recover their password using either username or email address
This section assumes you have run through the steps in Allow users to Sign In using their username or password.
Tell Devise to use :login in the reset_password_keys
- Modify config/initializers/devise.rb to have:
config.reset_password_keys = [ :login ]
Overwrite Devise’s finder methods in Users
- 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!) } ###the has some error in my issue, my you should comment two line bellow 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
- For Mongoid:
def self.find_record(login) found = where(:username => login).to_a found = where(:email => login).to_a if found.empty? found end
For Mongoid this can be optimized using a custom javascript function
def self.find_record(login) where("function() {return this.username == '#{login}' || this.email == '#{login}'}") end
- For MongoMapper:
def self.find_record(login) (self.where(:email => login[:login]).first || self.where(:username => login[:login]).first) rescue nil end
Update your views
- Modify the views
- passwords/new.html.erb:
- <p><%= f.label :email %><br /> - <%= f.email_field :email %></p> + <p><%= f.label :login %><br /> + <%= f.text_field :login %></p>
- passwords/new.html.erb:
Gmail or me.com Style
Another way to do this is me.com and gmail style. You allow an email or the username of the email. For public facing accounts, this has more security. Rather than allow some hacker to enter a username and then just guess the password, they would have no clue what the user’s email is. Just to make it easier on the user for logging in, allow a short form of their email to be used e.g “someone@domain.com” or just “someone” for short.
before_create :create_login def create_login email = self.email.split(/@/) login_taken = User.where( :login => email[0]).first unless login_taken self.login = email[0] else self.login = self.email end end def self.find_for_database_authentication(conditions) self.where(:login => conditions[:email]).first || self.where(:email => conditions[:email]).first end
For the Rails 2 version (1.0 tree): There is no find_for_database_authentication
method, so use self.find_for_authentication
as the finding method.
def self.find_for_authentication(conditions) conditions = ["username = ? or email = ?", conditions[authentication_keys.first], conditions[authentication_keys.first]] super end