Param update also sending password nil - ruby-on-rails

I have update method where I have all user fields, I have also password input field, if use does not want to change password, I want to update withotu changing password, but if he chagnges password I want to update password too. But if I add params :password I get errror that password is required field. Following is my controller update method
class UsersController < ApplicationController
before_action :set_user, only: %i[edit update]
def update
#user.attributes = update_params
if #user.save
flash[:notice] = 'User has been successfully updated.'
redirect_to admin_user_path
else
flash[:alert] = 'Please fill all required fields'
redirect_to new_admin_user_url(user: user_params)
end
def user_params
params.require(:user).permit(:first_name,
:last_name,
:email,
:division_id,
:password,
:employee_id)
end
def set_user
#user = User.find(params[:id])
end
end
I am using same params for create method, this one is condensed code as actually code is very large. If I remove :password then it will update other fields, but I want if password is blank then it should not update neither validate, but if it is not empty than it should update password too.

It's because when you submits your form with password field blank, params looks like this:
{
"user": {
"first_name": "Jack",
"last_name": "Sparrow",
"email": "jack#sparrow.com",
"division_id": 3,
"password": "",
"employee_id": 5
}
}
So you are saying "update password to be blank" and your validations doesn't allow this. Just try removing param password when not needed:
def update
if params[:user][:password].blank?
params[:user].delete("password")
end
#user.attributes = update_params
if #user.save
flash[:notice] = 'User has been successfully updated.'
redirect_to admin_user_path
else
flash[:alert] = 'Please fill all required fields'
redirect_to new_admin_user_url(user: user_params)
end
end # Also, this end is missing in your code

Rails 6.1+
Few methods are added in Rails 6.1 in ActionController::Parameters to clean the params that you can use. The methods that are added are, compact and compact! in order to remove nil values from params hash and compact_blank and compact_blank! in order to remove blank values from params hash.

Related

Devise let me to change new password as empty

This is my code for changing a password:
class RegistrationsController < Devise::RegistrationsController
protect_from_forgery
def update_password
if current_user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
sign_in(current_user, bypass: true)
redirect_to settings_path, notice: "updated"
else
redirect_to settings_path, alert: current_user.errors.full_messages
end
end
protected
def update_resource(resource, params)
resource.update_without_password(params)
end
def after_sign_up_path_for(_resource)
end
def after_update_path_for(_resource)
settings_path
end
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
end
private
def user_params
# NOTE: Using `strong_parameters` gem
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
end
The code works if I enter my current password and set a new one, but it doesn't when I enter a correct current password and as a new password + confirmation I leave empty fields (empty string).
The password will not get changed as "no password", but I get a flash message "updated". How do prevent that? I can think of this:
if current_user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
if params[:user][:password].blank?
redirect_to settings_path, alert: "blank pswd"
return
end
sign_in(current_user, bypass: true)
redirect_to settings_path, notice: "Your password has been updated!"
else
redirect_to settings_path, alert: current_user.errors.full_messages
end
However, this solution is a bit... ugly. Is there a more elegant way to handle this situation?
Thank you in advance
Reading your question I was a bit surprised about this behavior. But looking at the source code I could confirm that this behavior is expected and describe in a comment in the code:
# This method also rejects the password field if it is blank (allowing
# users to change relevant information like the e-mail without changing
# their password). In case the password field is rejected, the confirmation
# is also rejected as long as it is also blank.
I have mixed feelings about how to handle it. I would probably not show an error message because when the user didn't enter a new password they probably would not expect the password to change.
But another way to handle the error message could be to not handle that case in the method at all but to use a before_action:
before_action :ensure_new_password_provided, only: :update_password
private
def ensure_new_password_provided
return if params[:user][:password].present?
redirect_to settings_path, alert: "blank pswd"
end

Rails authentication from scratch, skip current password validation

I have an auth system from scratch, and when a user clicks on 'edit profile' it has to input the current password no matter the field he wants to edit.
def update
if params[:user][:password].present?
authenticated = #user.authenticate(params[:user][:current_password])
if authenticated && #user.update(user_params)
redirect_to root_url
flash[:notice] = "Your profile was successfully updated!"
else
#user.errors.add(:current_password, 'is invalid') unless authenticated
render :edit
end
elsif #user.update(user_params)
redirect_to root_url
flash[:notice] = "Your profile was successfully updated!"
else
render :edit
end
end
How can I call authenticate or use some context model validation only for the scenario when the user wants to change his password?
I wouldn't recommend mixing this logic into the model because you end up with complexity that is hard to follow as your application grows over time.
Try taking a look into form objects:
Form-backing objects for fun and profit
Railscast #416 Form Objects [paid subscription required]
I'd implement something like this:
class UserUpdateForm
include ActiveModel::Model
# Attributes
attr_accessor :user, :new_password, :new_password_confirmation
# Validations
validates :current_password, if: :new_password
validate :authenticate, if: :current_password
validates :new_password, confirmation: true, allow_blank: true
def initialize(user)
self.user = user
end
def submit(params)
self.new_password = params[:new_password]
self.new_password_confirmation = params[:new_password_confirmation]
if self.valid?
# Set other attributes as needed, then set new password below.
self.user.password = self.new_password if self.new_password.present?
self.user.save
else
false
end
end
private
def authenticate
unless self.authenticate(self.current_password)
self.errors.add(:current_password, 'is invalid')
end
end
end
Then you can call it from your controller like so:
def update
#user_update_form = UserUpdateForm.new(#user)
if #user_update_form.submit(params)
flash[:notice] = "Your profile was successfully updated!"
redirect_to root_url
else
render :edit
end
end
See the links above for how to handle the view and such. This is just to get you started.
You may create a nested if-else in this action statement that will check for existence of new_password and new_password_confirmation (or whatever the new password and confirmation fields are called) in the params[:user] object. If they are present - you may redirect to some king of page with request to enter existent password.
Another way is to use ajax to show asynchronously the dialog box with the same request (like respond_with self-invoking javascript function that handles that). Then handle submit button in of the dialog in the other action of the controller.
Update (considering use of validators):
Considering validation you may write your own validator (for password) and condition to check when the new password field come with some data from the client.
I think it could look like this:
validate :password_update?
def password_update?
if new_password.present?
if current_password !== self.password
errors.add(:current_password, "must be supplied!")
else
# update data and password
end
else
# do your regular update
end
end

Custom Update Function for Devise. Requiring current password only when updating password

I currently have a user that has extra attributes that I want them to be able to update without entering their current password.
So I had to create my own RegistrationsController and override the update method. (Taken from Devise github page).
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
# required for settings form to submit when password is left blank
if account_update_params[:password].blank?
account_update_params.delete('password')
account_update_params.delete('password_confirmation')
end
#user = User.find(current_user.id)
if #user.update_attributes(account_update_params)
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case their password changed
sign_in #user, :bypass => true
redirect_to edit_user_registration_path
else
render 'edit'
end
end
But now, I want to require them to put in their correct current password if they want to change their password. This seems to be an issue because if they do put in their current password, I have to add it to the permitted parameters. But then it tells me that current_password is not a valid attribute of the user.
def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:name, :email, :attr1, :attr2,
:password, :password_confirmation) }
end
Use update_with_password and update_without_password mehotds
with_password = params[:current_password].present?
if #user.update_user(with_password)
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case their password changed
sign_in #user, :bypass => true
redirect_to edit_user_registration_path
else
render 'edit'
end
private
def update_user(with_password)
if with_password
#user.update_with_passoword(account_update_params)
else
#user.update_passoword(account_update_params)
end
end
See documentation

How to separate change password from devise form

I am trying to do two things:
1) Change the default "edit user form" - provided with devise - to remove "password" and allow the other fields to be updated without having to enter a password ie remove the default validation for password.
2) Create a separate form for changing password
I have got everything to work, there is only one problem, in the separate form for updating password, I have included a field for current password. When using the form, no validation is made for current password, so I changed
#user.update_attributes(params[:user])
to
#user.update_with_password(params[:user])
This worked, however it raised another issue. Back in the main form with all the other details except password, the form now asks for a "current password". How can I achieve this without a validation for current password being called on the main form?
here is my registrations controller:
def update
#user = User.find(current_user.id)
if #user.update_attributes(params[:user])
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
clean_up_passwords(resource)
respond_with_navigational(resource) do
if params[:change_password] # or flash[:change_password]
render :change_password
else
render :edit
end
end
end
end
Thanks!
Solution 1
I have found a solution to the problem (albeit a very messy one):
def update
#user = User.find(current_user.id)
if params[:user][:password].blank?
if #user.update_attributes(params[:user])
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
respond_with_navigational(resource) do
render :edit
end
end
else
if #user.update_with_password(params[:user])
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
clean_up_passwords(resource)
respond_with_navigational(resource) do
render :change_password
end
end
end
Solution 2
Can you suggest a better solution?
Did you bother to check out Devise wiki? There are examples for both this cases
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-password
You should be looking at #user.update_with_password(params[:user]) vs #user.update_attributes(params[:user])
The accepted answer does not fully address the question. Which, I believe is to have a separate form for user profile attributes (like email, first name, etc) vs. the password. Here's what you need to do for that:
First, leverage the Devise::RegistrationsController for your profile updates.
Customize the view and remove the password and password_confirmation fields. Devise ignores these if they are not present in the put.
If you don't want to require the current password to make profile changes, read this. Not recommended; not secure.
Second, create your own controller to manage the password updates and your own helper to require current_password, password, and password_confirmation on update.
class PasswordsController < ApplicationController
before_filter :authenticate_user!
def edit
#user = current_user
end
def update
#user = User.find(current_user.id)
if #user.update_password_with_password(user_params)
# Sign in the user by passing validation in case their password changed
sign_in #user, :bypass => true
redirect_to edit_password_path, flash: { success: "Successfully updated password" }
else
render "edit"
end
end
private
def user_params
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
end
Here's the helper, update_password_with_password that will require the new password fields.
class User < ActiveRecord::Base
def update_password_with_password(params, *options)
current_password = params.delete(:current_password)
result = if valid_password?(current_password)
update_attributes(params, *options)
else
self.assign_attributes(params, *options)
self.valid?
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
clean_up_passwords
result
end
end

Rails dealing with blank params at controller level

I have a User model:
class User < ActiveRecord::Base
has_secure_password
# validation lets users update accounts without entering password
validates :password, presence: { on: :create }, allow_blank: { on: :update }
validates :password_confirmation, presence: { if: :password_digest_changed? }
end
I also have a password_reset_controller:
def update
# this is emailed to the user by the create action - not shown
#user=User.find_by_password_reset_token!(params[:id])
if #user.update_attributes(params[:user])
# user is signed in if password and confirmation pass validations
sign_in #user
redirect_to root_url, :notice => "Password has been reset."
else
flash.now[:error] = "Something went wrong, please try again."
render :edit
end
end
Can you see the problem here? A user can submit a blank a password/confirmation and rails will sign them in, because the User model allows blank on update.
It's not a security concern, since an attacker would still need access to a user's email account before they could get anywhere near this action, but my problem is that a user submitting 6 blank chars would be signed in, and their password would not be changed for them, which could lead to confusion later on.
So, I've come up with the following solution, and I'd like to check if there's a better way of doing it, before I push to production:
def update
#user=User.find_by_password_reset_token!(params[:id])
# if user submits blank password, add an error, and render edit action
if params[:user][:password].blank?
#user.errors.add(:password_digest, "can't be blank.")
render :edit
elsif #user.update_attributes(params[:user])
sign_in #user
redirect_to root_url, :notice => "Password has been reset."
else
flash.now[:error] = "Something went wrong, please try again."
render :edit
end
end
Should I be checking for nil as well as blank? Are there any rails patterns or idiomatic ruby techniques for solving this?
[Fwiw, I've got required: true on the html inputs, but want this handled server side too.]
Please try this:
we can use - present?
Ex:
if !params[:user][:password].present?

Resources