How do I validate password on the base of a condition? - ruby-on-rails

I want the password should be mandatory for web registration only and not for mobile app registration.
My validation code is below:
class User < ApplicationRecord
validate :password_for_web
validate :password_confirmation_for_web
def password_for_web
if !app_user && password.blank?
errors.add(:password, "can't be blank.")
end
end
def password_confirmation_for_web
if !app_user && password != self.password_confirmation=
errors.add(:password_confirmation, "doesn't match.")
end
end
end
Validation is working properly but when in case of mobile app registration it is still requiring password. Help regarding the issue would be appreciable.

You can use the if: and unless: options to toggle validations:
class User < ApplicationRecord
validates :password, presence: true, confirmation: true, unless: :app_user?
# or
validates :password, presence: true, confirmation: true, if: :web_user?
# TODO implement app_user?
end
You can pass a symbol (a method name) or a lambda.

If you are using simple rails app with responsive design.
You need to first check device is mobile or other device. This you can do in many ways.
Custom Way :
In application_helper.rb:
def mobile_device
agent = request.user_agent
return "tablet" if agent =~ /(tablet|ipad)|(android(?!.*mobile))/i
return "mobile" if agent =~ /Mobile/
return "desktop"
end
Then you can use it in your views:
<% if mobile_device == "mobile" %>
//add extra parameter to check in model
<% form_tag :mobile_device, true %>
<% end %>
In your model :
class User < ApplicationRecord
validate :password_for_web, if: :mobile_device?
validate :password_confirmation_for_web, if: :mobile_device?
def password_for_web
if !app_user && password.blank?
errors.add(:password, "can't be blank.")
end
end
def password_confirmation_for_web
if !app_user && password != self.password_confirmation=
errors.add(:password_confirmation, "doesn't match.")
end
end
def mobile_device?
mobile_device.present?
end
end
You can also use gems to check mobile device like:
https://github.com/fnando/browser
https://github.com/shenoudab/active_device
If you have separate mobile app.
Add extra parameter in your registration form for mobile application like I used in my view with name mobile_device. Use updated model code and you are done.

Related

Partial validations with rails wizard form

I would like to validate some fields according to tab in the url params
class Web < ApplicationRecord
validate :validate_website, if: -> {params[:tab] == 'address'}
private
def validate_website
regexp = %r{\A(http://www\.|https://www\.|http://|https://)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?\z}
if website.present? && !website.match(regexp)
errors.add :website, "Error"
end
end
I also tried :
validate :validate_website, if: :tab_address
def tab_address
params[:tab] == "address"
end
But no validation works

Ruby on Rails update user attributes without password

I am trying to create a means for a user to update their details without a password. I am using BCrypt with has_secure_password in my user model so that when a user signs up or changes password, the password and password_confirmation fields are checked to match before being saved to password_digest. I also have the following validation for setting a password:
validates :password, presence: true, length: { minimum: 5 }
As is users are able to update their password fine (so long as it meets the validations by being at least 5 characters long). But if a user tries to update their details (I have separate views/forms for updating password and updating other non-required attributes) then it gives an error saying "Password can't be blank, Password is too short (minimum is 5 characters)"
I have read through a lot of previous stackoverflow questions/answers and the closest I've got is adding , allow_blank: true to the end of the validation. This pretty much works as I want it to as BCrypt handles the validation if the password is blank so when signing up a user can't give a blank password, and users are able to change their details without a password. However when updating a password if the user gives a blank password then something weird happens. The blank password doesn't save (As it shouldn't since because BCrypt still needs a matching password and password_confirmation, which can't be blank), however the controller acts as though it does and passes the notice "password updated.". Here is my code in the controller:
def update
if Current.user.update(password_params)
redirect_to root_path, notice: "Password updated."
else
render :edit
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
I am particularly confused as to why Current.user.update(password_params) seems to be returning true (hence redirecting to the root path and passing the notice mentioned before) but the password definitely hasn't been updated. I have checked by logging out and back in again with the previous password and a blank password does not allow me to log in.
In cases like these, your best chance is to go to the source code of has_secure_password. You will find following code there:
define_method("#{attribute}=") do |unencrypted_password|
if unencrypted_password.nil?
self.public_send("#{attribute}_digest=", nil)
elsif !unencrypted_password.empty?
instance_variable_set("##{attribute}", unencrypted_password)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
when you call has_secure_password this code will create setter where attribute is password. It will dynamically create following method:
def password=(unencrypted_password)
if unencrypted_password.nil?
self.password_digest = nil
elsif !unencrypted_password.empty?
#password = unencrypted_password
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
This method then gets called when you assign attributes to your model:
Current.user.update(password_params)
As you can see newly defined method contains simple condition with two branches:
If you set password to nil it will delete the password_digest
If you set password to nonempty string it will store password hash to password_digest attribute
That is all. When your user sends empty password this method decides to do nothing and ignores the empty string, it doesn't get automatically assigned as empty password. Model pretty much completely ignores input (if password_confirmation is also empty), nothing gets saved, no validation is violated which means that Current.user.update(password_params) returns success and your controller proceeds with "Password updated." branch.
Now, when you know about this behaviour what can you do about it?
Let's say you model looks like this
class User < ApplicationRecord
has_secure_password
validates :password, presence: true, length: { minimum: 5 }
end
And you want your validation to work in two cases
When new user is created
When users update their password
First case is easy, as you mentioned in your question if you use allow_blank: true instead of presence: true validation is handled by has_secure_password if password is empty and if it is not, password will proceed through the validation.
But than we get to other requirements:
User has to be able update other attributes then just password
We still want to validate password if it gets updated
These two requirements rule out the presence: true part of the validation, it cannot be there. Same thing applies to allow_blank: true. In this case you probably want to use conditional validation:
class User < ApplicationRecord
has_secure_password
validates :password, length: { minimum: 5 }, if: :password_required?
private
def password_required?
password.present?
end
end
This code ensures that validation is executed every time user fills in the password. It doesn't matter if it's create or update action.
Now to the last thing.
What if user leaves empty password.
From your question I suppose you want to show validation error if user sends empty password. From description above User model doesn't know that it should validate the password at all. You have to tell your model about it eg. like this:
class User < ApplicationRecord
has_secure_password
validates :password, length: { minimum: 5 }, if: :password_required?
def enforce_password_validation
#enforce_password_validation = true
end
private
def password_required?
#enforce_password_validation || password.present?
end
end
Now you could use it like this in your controller:
def update
user = Current.user
user.enforce_password_validation
if user.update(password_params)
redirect_to root_path, notice: "Password updated."
else
render :edit
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
Now validation should fail even if user submits empty password.
I made the same configuration as the first answer.
This my User.rb model
class User < ApplicationRecord
validates :email, presence: true
validates :password, length: { minimum: 6 }, if: :password_required?
validates :password_confirmation, length: { minimum: 6 }, if: :password_required?
has_secure_password
private
def password_required?
false || password.present?
end
end
It worked adding false at the beginning of the method.
Now, in my controller, I have this:
class UsersController < ApplicationController
...
def confirm_email
user = User.find_by(token: params[:token])
user.update(is_confirmed?: true)
redirect_to login_path, notice: "You've confirmed your account"
end
...
end
I hope this helps!
For the validation of the password in your model try this instead :
validates :password, presence: true, length: { minimum: 5 }, on: :create
This line mean that the validation will be only call on :create method not :update anymore. So that you can remove the allow_bank and your update password will work fine

Ruby on Rails SystemStackError stack level too deep validate Wicked gem

Calling validate :method_name causes an infinite loop SystemStackError.
Doing the validation directly inline allows the validation to pass without error.
There must be something I'm overlooking or doing wrong...
The validations worked fine when doing them directly in the model instead of within the conditional_reservation_validation.
Example code that causes SystemStackError stack level too deep
The basic process has been built following this example:
Building Partial Objects Step by Step
I know this needs to be refactored / cleaned up.
Snippet from model:
validate :conditional_reservation_validation
def conditional_reservation_validation
if created_by_admin?
validates_presence_of :vehicle_location
validates_presence_of :pickup, :dropoff, :departure_date, :departure_time, :passengers
else
if active_or_parking?
validates_presence_of :vehicle_location
# SystemStackError occurs when validate :parking_agreement_if_location fires
validate :parking_agreement_if_location
end
if active_or_pickup?
# additional validations
end
end
# ...
end
def parking_agreement_if_location
if vehicle_in_special_parking_location(vehicle_location)
if self.parking_agreement == true
# ok
else
self.errors.add :base, "Must read and understand parking instructions."
end
end
end
def vehicle_in_special_parking_location(vehicle_location)
parking_locations = Location.where(require_parking: true)
if parking_locations.include?(vehicle_location)
return true
else
return false
end
end
# methods to check the step in process
def active_or_parking?
status.include?('parking') || active?
end
Calling validate :parking_agreement_if_location triggers a SystemStackError
Example code that stops the error:
Just taking the code out of the :parking_agreement_if_location method and putting it directly inline stops the SystemStackError.
validate :conditional_reservation_validation
def conditional_reservation_validation
if created_by_admin?
validates_presence_of :vehicle_location
validates_presence_of :pickup, :dropoff, :departure_date, :departure_time, :passengers
else
if active_or_parking?
validates_presence_of :vehicle_location
if vehicle_location
locations = Location.where(require_parking: true)
if locations.include?(vehicle_location)
if parking_agreement == true
# ok
else
self.errors.add :base, "Must read and understand parking instructions."
end
# validate :parking_agreement_if_location
end
end
end
if active_or_pickup?
# These methods cause SystemStackError as well...
validate :pickup_date_in_range
validate :pickup_date_in_future
end
end
end
Controller update action:
def update
params[:reservation][:status] = step.to_s
params[:reservation][:status] = 'active' if step == steps.last
case step
when :parking
#reservation.assign_attributes(reservation_params)
when :pickup
#reservation.assign_attributes(reservation_params)
when :billing
#reservation.assign_attributes(reservation_params)
end
render_wizard #reservation
end
You are using validations wrong way. They need to be invoked on class level.
You need to use conditional validations instead:
validates_presence_of :vehicle_location, if: :created_by_admin?
validates_presence_of :pickup, :dropoff, :departure_date, :departure_time, :passengers, if: :created_by_admin?
validates_presence_of :vehicle_location, unless: :created_by_admin?, if: :active_or_parking?
validate :parking_agreement_if_location, unless: :created_by_admin?, if: :active_or_parking?

RSpec pass validation test, which should be failed

I have a Company model with attr_accessor :administrator, so when user creates company, he also need to fill some fields for administrator of this company. I'm trying to test, that he fill all fields correctly.
class Company < ActiveRecord::Base
attr_accessor :administrator
validates :name, presence: true
validates :administrator, presence: true, if: :administrator_is_valid?
private
def administrator_is_valid?
administrator[:name].present? and
administrator[:phone].present? and
administrator[:email].present? and
administrator[:password].present? and
administrator[:password_confirmation].present? and
administrator[:password] == administrator[:password_confirmation]
end
end
company_spec.rb is:
require 'rails_helper'
describe Company do
it 'is valid with name and administrator' do
company = Company.new(name: 'Company',
administrator: {
name: nil,
email: nil,
phone: nil,
password: 'password',
password_confirmation: ''
})
expect(company).to be_valid
end
end
So, as you see, I have a lot of mistakes in validation test, but RSpec pass it.
Thanks!
That's because you didn't construct your validation properly. See, if: administrator_is_valid? will return false for your test, telling Rails to skip this validation rule.
I suggest you drop using the presence validator in favor of using administrator_is_valid? method as a validation method, because after all, if the administrator is valid then it is present. The code should look like this
validate :administrator_is_valid?
private
def administrator_is_valid?
(administrator[:name].present? and
administrator[:phone].present? and
administrator[:email].present? and
administrator[:password].present? and
administrator[:password_confirmation].present? and
administrator[:password] == administrator[:password_confirmation]) or
errors.add(:administrator, 'is not valid')
end
You could clean up your code like this:
validate :administrator_is_valid?
private
def administrator_is_valid?
if administrator_cols_present? && administrator_passwords_match?
true
else
errors.add(:administrator, 'is not valid')
end
end
def administrator_cols_present?
%w(name phone email password password_confirmation).all? do |col|
administrator[col.to_sym].present? # or use %i() instead of to_sym
end
end
def administrator_passwords_match?
administrator[:password] == administrator[:password_confirmation]
end
Another improvement might be to move your administrator to a struct, then call valid? on the object.
admin = Struct.new(cols) do
def valid?
cols_present? && passwords_match?
end
def cols_present?
cols.values.all? { |col| col.present? }
end
def passwords_match?
cols[:password] == cols[:password_confirmation]
end
end
Then:
validate :administrator_is_valid?
def admin_struct
#admin_struct ||= admin.new(administrator)
end
def administrator_is_valid?
errors.add(:administrator, 'is not valid') unless admin_struct.valid?
end

Rails: update user but password is invalid

I have a payment api that takes bank account info and user info. I catch the api response and use ajax to send the infomation into my controller where I try to save the information to my user. When I save I get the error Validation failed: Password can't be blank, Password is invalid: Any ideas?
Bank Controller:
def addbank
#user = current_user
#user.phone_number = params[:phone_number]
#user.birth_year = params[:year]
#user.bank_uri = (params['bank_acct_uri'])
#user.save! # <------- ERROR here!
# Code was removed to be more clear
end
User Controller:
def update
# update user controller has been commented out but the error is still there
end
User Model:
class User < ActiveRecord::Base
attr_accessible :email,:password,:password_confirmation,:phone_number,:birth_year
attr_accessor :password
before_save :encrypt_password
before_save { |user| user.email = email.downcase }
VALID_PASSWORD_REGEX = # some reg-expression
VALID_PHONE = # some reg-expression
validates_confirmation_of :password
validates :password, presence: true, format:{ with: VALID_PASSWORD_REGEX }
validates :phone_number, format: { with: VALID_PHONE }, if: :phone_number
end
Edit: Why is saving user not hitting my update user controller?
If you want to avoid the validation of one particular field (password in your case), but you want to do all the other validations (for example phone number), you can do something like this:
attr_accessor :skip_password
validates :password, presence: true, format:{ with: VALID_PASSWORD_REGEX }, unless: :skip_password
Then, in your controller, you could do:
def addbank
#user = current_user
#user.skip_password = true # We don't want to validate this..
#user.phone_number = params[:phone_number]
#user.birth_year = params[:year]
#user.bank_uri = (params['bank_acct_uri'])
#user.save! # <------- ERROR here!
# Code was removed to be more clear
end
Do this at your own risk ~~
Where are you storing the encrypted password?
If you store it in password then it will fail validation every save because it doesn't equal password_confirmation.
I'd recommend putting the password in a separate field.
#from the User Model
attr_accessor :password, :password_confirmation
validates_presence_of :password, :on => :create
validates_confirmation_of :password
def password=(password)
#password = password
self.password_digest = BCrypt::Password.create(#password, :cost => 14)
end
def authenticate(password)
BCrypt::Password.new(self.password_digest) == password
end
This way the password gets hashed and saved to password_digest and won't trigger a validation error on save.
You can try to save without validation:
#user.save(:validate => false)
UPDATE:
if !#user.valid? && #user.errors[:phone_number].any?
#do not save
else
#user.save(:validate => false)
end
I'll post this as I am about 95% certain this is the cause, but I apologize if I'm off.
I believe the cause of this is because the user's password is indeed blank. If you look at your database, you'll see a column probably called encrypted_password, which is never directly accessed via your model, nor is it ever de-crypted into your accessible password and password_confirmation attributes.
In order to update the user, you will have to enter re-enter the password, or use the save(false) method to (potentially dangerously) bypass validations.

Resources