-
Notifications
You must be signed in to change notification settings - Fork 5.5k
How To: Allow users to sign_in using their username or email address
h2. Allow users to Sign In using their username or email address
For this example, we will assume your model is called Users
h3. Create a username field in Users
rails generate migration add_username_to_users username:string
rake db:migrate
attr_accessible :username
h3. Create a login virtual attribute in Users
# 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
h3. Tell Devise to use :login in the authentication_keys
config.authentication_keys = [ :login ]
- 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
h3. Update your views
Rails 3:
rails g devise:views
Rails 2:
script/generate devise_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>
h3. Manipulate the :login label that Rails will display
Rails 2:
activemodel:
attributes:
user:
login: "Username or email"
Rails 3:
en:
activerecord:
attributes:
user:
login: "Username or email"
h2. 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.
h3. Tell Devise to use :login in the reset_password_keys
config.reset_password_keys = [ :login ]
h3. 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!) }
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":http://omarqureshi.net/posts/2010/06/17/mongoid-or-query/
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
h3. Update your 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>
h2. 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