79
votes

I want to update users attributes without password in devise. The case is like, if password and password confirmation fields are not blank then I need devise error and if they are blank then other user attributes need to be updated. How could I do this with devise?

Thanks in advance!

19

19 Answers

66
votes

Is this what you're look for? From the Devise wiki

Allow users to edit their account without providing a password

It shows how to do it for rails 3 and rails 4

=======================

John's solution is a simpler alternative

95
votes

I think this is a much better solution:

if params[:user][:password].blank? && params[:user][:password_confirmation].blank?
    params[:user].delete(:password)
    params[:user].delete(:password_confirmation)
end

This prevents you from having to change the Devise controller by simply removing the password field from the form response if it is blank.

Just be sure to use this before @user.attributes = params[:user] or whatever you use in your update action to set the new parameters from the form.

21
votes

I think with Devise 3.2+ you can just override update_resource in your subclassed controller. Here's an example for the question originally asked:

class MyRegistrationsController < Devise::RegistrationsController
  protected

  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end
8
votes

I solved this problem as follows: if form submitting with password, then need fill current_password field or form updated without password and current_password

in model:

class User
  devise: bla-bla-bla

  attr_accessor :current_password
end

In controller:

class Users::RegistrationsController <  Devise::RegistrationsController
  layout 'application'


   def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)


    # custom logic
    if params[:user][:password].present?
      result = resource.update_with_password(params[resource_name])
    else
      result = resource.update_without_password(params[resource_name])
    end

    # standart devise behaviour
    if result
      if is_navigational_format?
        if resource.respond_to?(:pending_reconfirmation?) && resource.pending_reconfirmation?
          flash_key = :update_needs_confirmation
        end
        set_flash_message :notice, flash_key || :updated
      end
      sign_in resource_name, resource, :bypass => true
      respond_with resource, :location => after_update_path_for(resource)
    else
      clean_up_passwords resource
      respond_with resource
    end
  end
end
5
votes

If you still want to support password change but make it optional, check the availability of current_password as such:

class MyRegistrationsController < Devise::RegistrationsController
  protected

  def update_resource(resource, params)
    if params[:current_password].blank?
     resource.update_without_password(params.except(:current_password))
    else
      resource.update_with_password(params)
    end
  end
end

That way if the current_password is present you can proceed and update the password, else ignore it and update without password.

5
votes

params[:user][:password] is not working in rails 6

You have to change params[:user][:password] to params[:password]

Remove [:user] in all params

my source code is below

before run this command

rails generate devise:controllers users -c=registrations

class Users::RegistrationsController < Devise::RegistrationsController
  # before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  protected

  def update_resource(resource, params)
    puts "params ===> #{params}"
    if params[:password].blank? && params[:password_confirmation].blank?
      params.delete(:password)
      params.delete(:password_confirmation)
      params.delete(:current_password)
      resource.update_without_password(params)
    else
      super
    end
  end
end
3
votes

I solve this problem with my interface. There are two ways it can go.

Disable the fields if they are empty

This bit of jQuery disables the fields before the form submits in the event that they are empty. It requires a certain markup pattern, check the demo on Codepen.

$(".password-fields").each(function() {
  var $fields, $form;
  $form = $(this).parents("form");
  $fields = $(this).find("input");
  $form.on("submit", function() {
    $fields.each(function() {
      if ($(this).val() === "") {
        $(this).attr("disabled", "disabled");
      }
    });
  });
});

Give the user a checkbox to select if they want to update

Another option is to remove the password fields from the form if the user hasn't indicated that they want to update their password. Here's the example on CodePen.

$(".password-fields").each(function () {
  var $fields, $button, html;
  $fields = $(this);
  $button = $fields.prev(".update-password").find("input");
  html = $fields.html();

  $button
    .on("change", function () {
      if ( $(this).is(":checked") ) {
        $fields.html(html);
      }
      else {
        $fields.html("");
      }
    })
    .prop("checked", "checked")
    .click();
});

In either case, it requires no update to the app itself. You are just submitting the fields you want to change.

3
votes

I instead override #password_required? method inside user model.

class User < ActiveRecord::Base
  devise :database_authenticatable, :validatable #, ...

  def password_required?
    if respond_to?(:reset_password_token)
      return true if reset_password_token.present?
    end
    return true if new_record?
    password.present? || password_confirmation.present?
  end
end

So if user is new he will be required to specify password. However exist user will be required to specify password only if he fills in either password or password_confirmation attributes.

For more details see: https://github.com/plataformatec/devise/blob/master/lib/devise/models/validatable.rb#L33

My implementation is almost identical as original: https://github.com/plataformatec/devise/blob/master/lib/devise/models/validatable.rb#L53

except that I check for presence (which returns false for blank strings)

Here's discussion about my pull request on this matter: https://github.com/plataformatec/devise/pull/3920

2
votes

Anyone visiting in 2020, this is the simplest solution I found:

in registrations_controller.rb:

class RegistrationsController < Devise::RegistrationsController

  protected

  def update_resource(resource, params)
    # Require current password if user is trying to change password.
    return super if params["password"]&.present?

    # Allows user to update registration information without password.
    resource.update_without_password(params.except("current_password"))
  end
end

In routes.rb:

devise_for :users, controllers: {
  registrations: 'users/registrations'
}

Full credit to: Masatoshi Nishiguchi

https://www.mnishiguchi.com/2017/11/24/rails-devise-edit-account-without-password/

1
votes

If you want Devise to check current password only when user tries to change password (that means you can change other attributes without providing current password):

class RegistrationsController < Devise::RegistrationsController

  protected

    def update_resource(resource, params)
      if params[:password].blank? && params[:password_confirmation].blank?
      resource.update_without_password(params)
    else
     super
    end
  end
end

Also in your model:

attr_accessor :current_password

And don't forget about

devise_for :users, controllers: {registrations: 'registrations'}

in routes.rb.

1
votes

It's more business logic so I wouldn't put too much code in the controller. I suggest overriding Devise::Models::Validatable#password_required? in your model:

def password_required?
  new_record? || password.present? || password_confirmation.present?
end
1
votes

As workaround put in your User model

def password_required?
  encrypted_password.blank? || encrypted_password_changed?
end
1
votes

For future Googlers, I just spent 3 hours on this, here's what worked for me.

In my User model I remove :validatable and added validates method for the password and password_confirmation only if they are present:

class User < ApplicationRecord
  devise :database_authenticatable, :registranable, recoverable, :rememberable
    
  validates :password, length: { in: 6..128 }, if: lambda {self.password.present?}
  validates_confirmation_of :password, if: lambda {self.password.present?}
end

Then in my users_controller's update method I delete the password and password_confirmation params if they are blank

class UsersController > ApplicationController
  def update
    if params[:user][:password].blank?
      params[:user].delete(:password)
      params[:user].delete(:password_confirmation)
    end

    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: 'User was successfully updated' }
      else
        format.html { render :edit }
      end
    end
  end
end

Simple, plain, easy and no jerking around with Devise's convoluted way of doing things.

You're all welcome.

0
votes

I had the exact same issue and this the solution I came up with and believe me it works. What i did is create a second user_params method and named it user_params_no_pass anyway take a look: what is happening here is that the admin will provide a password when the password need to be updated else leave the password blank. when the password is blank the user_params_no_pass is used else user_params. I hope that helps

    def update

    if @user.valid_password?(params[:user][:password])
          respond_to do |format|

            if @user.update(user_params)
              format.html { redirect_to @user, notice: 'User profile was successfully updated.' }
              format.json { render :show, status: :ok, location: @user }
            else
             format.html { render :new }
             format.json { render json: @user.errors, status: :unprocessable_entity }
            end
          end
    else
      respond_to do |format|

            if @user.update(user_params_no_pass)
              format.html { redirect_to @user, notice: 'User profile was successfully updated without password.' }
              format.json { render :show, status: :ok, location: @user }
            else
              format.html { render :edit }
              format.json { render json: @user.errors, status: :unprocessable_entity }
            end
          end
    end
  end

  def destroy
     @user.destroy
      respond_to do |format|
        format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
        format.json { head :no_content }
      end
  end
  private

    def set_user
      @user = User.find(params[:id])
    end

    def user_params
      params.require(:user).permit(:user_name, :first_name, :middle_name, :last_name, :dob, :gender, :race, :hispanic, :leader, :mentor, :student, :email, :organization_id, :password, :opus,
  :release_date, :days_to_release, :current_level, :is_active)
    end


    def user_params_no_pass
      params.require(:user).permit(:user_name, :first_name, :middle_name, :last_name, :dob, :gender, :race, :hispanic, :leader, :mentor, :student, :email, :organization_id, :opus,
  :release_date, :days_to_release, :current_level, :is_active)
    end
0
votes

I find this slight variation on the code above easier to follow:

def update
  @user = User.find(params[:id])

  method = if user_params[:password].blank?
             :update_without_password
           else
             :update_with_password

  if @user.send(method, user_params)
    redirect_to @user, notice: 'User settings were saved.'
  else
    render :edit
  end
end
0
votes

After much exploration of the above possibilities I finally found a solution which allows you to update some attributes without a password and some with:

I made a view for user/edit.html.erb with the following form in it.

<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

  <%= f.submit "Save changes"%>
<% end %>

I set routes in routes.rb

resources :users, only: [:show, :index, :edit, :update]

I made edit and update methods in users_controller.rb

def edit
  @user = current_user
end

def update
  @user = current_user
  if @user.update_attributes(user_params)
    flash[:success] = "Profile updated"
    redirect_to root_url
  else
    render 'edit'
  end
end


def user_params
  params.require(:user).permit(:name, :avatar, :whatsup)
end

I used this edit view for changes which did not require a password. It skips the devise registration controller entirely because I link to it with

edit_user_path(current_user)

I also set the params so that email and password cannot be changed here. To update password and email, I link to the stock generated devise view:

edit_user_registration_path(current_user)

I admit that this is a huge work around but none of the simpler solutions solved all of the above problems.

0
votes

Better and short way: add check params into the user_params block. For example:

def user_params
    up = params.require(:user).permit(
      %i[first_name last_name role_id email encrypted_password
         reset_password_token reset_password_sent_at remember_created_at
         password password_confirmation]
    )

    if up[:password].blank? && up[:password_confirmation].blank?
      up.delete(:password)
      up.delete(:password_confirmation)
    end
    up
end
0
votes

to update attributes for user, you will need to use :current_password, to check 'do your user use correct current_password or someone wants to break your user'

so in form:

= f.input :current_password,
          hint: "we need your current password to confirm your changes",
          required: true,
          input_html: { autocomplete: "current-password" }

in controller:

    if your_user.valid_password?(params[:user][:current_password])
        your_user.update_attributes(user_params.except(:current_password, :password, :password_confirmation))
    end

the first line checks 'do your user sends the correct password or not', and then we can to update the attributes without garbage

0
votes

If you really want to update the password without the current_password then you need to use the user reset_password method

@user.reset_password(params[:user][:password], params[:user][:password_confirmation])

Here is a complete working solution to the problem that works in all scenarios:

  if params.dig(:user, :password).blank?
    updated = @user.update_without_password(params[:user].to_unsafe_hash)
  else
    if params.dig(:user, :current_password).nil?
      @user.reset_password(params[:user][:password], params[:user][:password_confirmation])

      updated = @user.update_without_password(params[:user].to_unsafe_hash)
    else
      updated = @user.update_with_password(params[:user].to_unsafe_hash)
    end

    bypass_sign_in(@user)
  end