case insensitive name, while preserving capitalization, in Devise - ruby-on-rails

Using a name as key, how do we validate the name when registering by ignoring case while still remembering the case when displaying?
In config/initializers/devise.rb, setting config.case_insensitive_keys = [ :name ] seems to lowercase the entire name before registering.
Example: some dude names himself TheFourthMusketeer.
The views will display TheFourthMusketeer, not thefourthmusketeer
No new user can register under, say, tHEfourthMUSKETEER

What you might try is to not set :name as case insensitive, which will properly save the case-sensitive name in the database:
config.case_insensitive_keys = []
Then, override the find_first_by_auth_conditions class method on User to find the user by their name. Note that this code will vary depending on the database (below is using Postgres):
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
where(conditions).where("lower(name) = ?", login.downcase).first
else
where(conditions).first
end
end
Doing this, a User.find_for_authentication(login: 'thefourthmusketeer') will properly return the record with a name of "TheFourthMusketeer".
See https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address for an explanation of overriding this method.

The accepted answer is incomplete because it's still case-sensitive on registration. So for example, 'username' and 'USERNAME' could both register successfully, but only the first would be able to login.
Disable case-insensitive keys in config/initializers/devise.rb (this can also be model-specific so check there too):
config.case_insensitive_keys = []
Overwrite the find_first_by_auth_conditions method of models/user.rb:
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:username)
where(conditions).where(["lower(username) = :value", { :value => login.downcase }]).first
else
where(conditions).first
end
end
...and also set validates_uniqueness_of in models/user.rb:
validates_uniqueness_of :username, :case_sensitive => false
So there you have it: case-insensitive authentication, with case-insensitive registration, that preserves case, in the database.

Related

How can I verify username or email are correct using Devise and Rails 4

I am using Devise and Rails 4.
I am working with login screen. In login screen i'm passing username/email and password but I have to check whether any one of the field username or email matched with any user in system.
Tried following condition to find user with username or email but it will be throwing an error for devise valid password method.
Condition:
user = User.where(["username = :value OR email = :value", { :value => username.downcase }])
after this i'm checking user password as like user.valid_password?(password)
Error: NoMethodError (undefined method `valid_password?' for #)
For following condition valid_password? will be working fine
user = User.find_by(email: email.downcase)
But i have to check both username and password in ::find_by method like as follows
user = User.find_by("email= email.downcase OR username= email.downcase")
Is there some way I can accomplish something like above?
This is most wise confuse error, when an single instance is mixed up wish a relation, you simply forgot #first select from the relation, since where returns relation, rather than find_by, which returns a record for single match. So just rewrite (for rails 5):
user = User.where(email: email.downcase).or(username: email.downcase).first
and for Rails 4 with arel:
users = User.arel_table
user = User.where(users[:email].eq(downcase).or(users[:username].eq(email.downcase))).first
or with partially plain SQL:
user = User.where(["username = :value OR email = :value", { :value => username.downcase }]).first

Devise `find_first_by_auth_conditions` method explanation

My two methods using Devise:
Method1
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
where(conditions).where(["lower(username) = :value OR lower(email)
= :value", {:value => signin.downcase }]).first
end
Method2
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
login = conditions.delete(:signin)
where(conditions).where(["lower(username) = :value OR lower(email) =
:value", {:value => login.strip.downcase }]).first
end
My questions:
What does this code perform/do? login = conditions.delete(:signin)
Without the above code I get an error undefined local variable or method signin
The following answers question 1)—specifically A) and B) below. The following code is an example and does not mirror the actual methods or arguments generated by Devise:
Here: the Hash contains :signin key-value pair and other valid ActiveRecord's #where syntax
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where
devise_conditions = {:signin => "cool#gmail.com", :deleted => false, :role => 'basic'}
#=> {:signin=>"cool#gmail.com", :deleted => false, :role => 'basic'}
This duplicates original argument to prevent modification in order to use it in subsequent methods or queries
http://ruby-doc.org/core-1.9.3/Object.html#method-i-dup
conditions = devise_conditions.dup
#=> {:signin=>"cool#gmail.com", :deleted => false, :role => 'basic'}
Here, the code: A) deletes the :signin key-pair from the Hash; and
B) assigns new variable signin with value of :signin key-pair from Hash
http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-delete
signin = conditions.delete(:signin)
#=> "cool#gmail.com"
The immediately above code could be rewritten to clarify both operations using additional "Element Reference" of Hash
http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-5B-5D
signin = conditions[:signin]
#=> "cool#gmail.com"
conditions.delete(:signin)
#=> "cool#gmail.com" # deleted value from Hash is returned
conditions
#=> {:deleted => false, :role => 'basic'}
The method's original argument has been preserved by using dup
devise_conditions
#=> {:signin=>"cool#gmail.com", :deleted => false, :role => 'basic'}
The following answers question 2):
Method1 does not create a variable signin. undefined local variable or method signin results from no signin variable being created when the code which creates it is removed.
Method2 creates a variable login which has the value from the original Hash named conditions with the key :signin.
This deletes signin key from conditions hash and assigns its value to login local variable.
2.. I guess you mean that signin is not defined in find_first_by_auth_conditions? Then I also guess that signin is an attribute of warden_conditions so you can try: warden_conditions.signin.

Devise `find_first_by_auth_conditions` explanation [duplicate]

This question already has answers here:
Devise `find_first_by_auth_conditions` method explanation
(3 answers)
Closed 8 years ago.
I am having a tough time understanding this code from Devise, even though I've read the documentation and done some research.
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
signin = conditions.delete(:signin)
where(conditions).where(["lower(username) = :value OR lower(email) =
:value", {:value => signin.downcase }]).first
end
Please explain the components of this portion of the above method:
where(conditions).where(["lower(username) = :value OR lower(email) =
:value", {:value => signin.downcase }]).first
# arg is Hash, so assign to variable and downcase
x = warden_conditions[:signin].downcase
# create duplicate to preserve orig
c = warden_conditions.dup
# delete `:signin`
c.delete(:signin)
# if email, only search for email
if x =~ /^[a-zA-Z0-9._-]+#[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/
y = self.where(c).where(:email => x) # self is implied, but optional--so we use it here for clarity
# if not email, only search for name
else
y = self.where(c).where(:username => x)
end
# y is array, so should be only one AR obj or empty array considering they are unique
# `Array#first` will return `nil` or AR obj
return y.first
regex via:
validate email with regex jquery
The above code considers all previous records for columns email and username to be stored as lowercase as follows:
before_save :downcase_fields
def downcase_fields
self.email.downcase
self.username.downcase
end

Find user using email + password in Devise

My Rails app uses data from legacy database. Imported users from this DB contain duplicate emails. Authentication doing with email+password (and it's a unique combination in DB).
Devise uses method find_for_database_authentication to find user. However params don't contain password (just login name).
What can I do?
try this :
find_user = User.where("email = ?", params["user"]["email"]).first
find_user.valid_password?(params["user"]["password"])
You can search this way
in the User model override the find method:
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
email = conditions.delete(:email)
pwd = conditions.delete(:password)
encrypted_pwd = User.new(password: pwd).encrypted_password
where(conditions).where(["lower(email) = :email AND encrypted_password = :pwd", { :email => email.strip.downcase, :pwd => encrypted_pwd }]).first
end
And probably config/initializers/devise.rb will require smth like:
config.authentication_keys = [ :email, :password ]

How do you validate the presence of one field from many

I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""

Resources