views:

856

answers:

3

Whats the best way to enable users to log in with their email address OR their username? I am using warden + devise for authentication. I think it probably won't be too hard to do it but i guess i need some advice here on where to put all the stuff that is needed. Perhaps devise devise already provides this feature? like in the config/initializers/devise.rb you would write:

config.authentication_keys = [ :email, :username ]

To require both username AND email for signing in. But i really want to have only one field for both username and email and require only one of them. I'll just visualize that with some ASCII art, it should look something like this in the view:

Username or Email:
[____________________]

Password:
[____________________]

[Sign In]
+1  A: 

Well, after a lot of searching i have found a solution for the problem. I'm not quite satisfied with it (i'd rather have a way to specify this in the initializer), but it works for now. in the user model i added the following method:

def self.find_for_authentication(conditions={})
  unless conditions[:email] =~ /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i # email regex
    conditions[:username] = conditions.delete("email")
  end
  super
end

this overrides the devise method find_for_authentication which normally just finds the first record based on the given conditions. this modified version checks, whether the user really typed an email address into the email field. if not, it falls back to checking for the username. This short Blog Entry covers exactly this topic and helped me a lot.

edit: the above code works, but i think it is prettier to do this (only rails):

def self.find_for_database_authentication(conditions={})
  self.where("username = ?", conditions[:email]).limit(1).first ||
  self.where("email = ?", conditions[:email]).limit(1).first
end

i am using MetaWhere for my find conditions though, where i would write:

def self.find_for_database_authentication(conditions={})
  self.where(:username.eq % conditions[:email]).limit(1).first ||
  self.where(:email.eq % conditions[:email]).limit(1).first
end
padde
+2  A: 
def self.find_for_authentication(conditions)
  conditions = ["username = ? or email = ?", conditions[authentication_keys.first], conditions[authentication_keys.first]]
  # raise StandardError, conditions.inspect
  super
end

Use their example!

Martin
using their example, shouldn't it be `def self.find_for_database_authentication(conditions)` ?
padde
btw, with their example, using rails 3.0.0.beta4 i got a `NoMethodError in Devise/sessionsController#create undefined method `assert_valid_keys' for ["username = ? or email = ?", "xxx", "xxx"]:Array`, that's why i used my own solution
padde
A: 

For me it's case sensitive, how do I remove that?

Thanks!

Alfred Nerstu
see my own answer ad replace `=` by `LIKE`, or, using MetaWhere (which i do because i like the idea of a `Future.where :sql.not_in => ['code']`) replace `:username.eq` with `:username.matches`. Same thing if you want to make the email case insensitive.
padde
you should also remove any '%' (wildcard for 0+ chars) and '_' (wildcard for 1 char) from your `conditions[:email]`. otherwise it would be possible to enter for example `%` instead of any username/email that starts with 'a' or 'A', and then just guess the password, which might be a serious security hole! So just do `conditions[:email].gsub(/[%_]/,'')` before finding the users. Limiting the results to one also helps to prevent this.
padde
nah sorry, you should use the mutator `gsub!` instead of `gsub`. and its still not perfect: you should also declare the username as unique by adding `validates_uniqueness_of :username` to your user model!
padde