devise ユーザー情報変更をcurrent_passwordなしで行う。

## はじめに

Deviseのデフォルトの状態だと、ユーザーのアカウントをアップデートするには、current_passwordが必要です。
ただ名前やメールアドレスといった基本情報の編集のたびにパスワードを入力させるのは、UIの面を考慮するとあまりよろしくないです。そこで、現在のパスワードを入力しなくてもプロフィールの情報を編集できるように実装します。また、パスワードを変更する場合は、現在のパスワードを入力するような仕様にします。
![スクリーンショット 2022-02-28 0.13.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/e7349a0b-db4c-19ba-3d73-97123bc62ca0.png)


## Devise::RegistrationsController#updateの実装について
Devise::RegistrationsController#updateでは、以下のとおり、Devise::RegistrationsController#update_resource が呼び出されています。

~~~ruby:registrations_controller.rb
def update    
    resource_updated=update_resource(resource,account_update_params)
end
~~~
参考: [devise/app/controllers/devise/registrations_controller.rb ](https://github.com/heartcombo/devise/blob/main/app/controllers/devise/registrations_controller.rb#L46)

### Devise::RegistrationsController#update_resource の実装

~~~ruby
  # By default we want to require a password checks on update.
  # You can overwrite this method in your own RegistrationsController.
  def update_resource(resource, params)
    resource.update_with_password(params)
  end
~~~
参考:[devise/app/controllers/devise/registrations_controller.rb ](https://github.com/heartcombo/devise/blob/451ff6d49c71e543962d2b29d77f2e744b2d47e1/app/controllers/devise/registrations_controller.rb#L93)

説明書きから次のことがわかります。

- デフォルトで、アップデート時にパスワードをチェックされる。
- RegistrationsControllerオーバーライドできる。

つまり、このupdate_resourceメソッドを自身の定義するRegistrationsControllerでオーバーライドすれば、ユーザー情報のアップデート時の振る舞いをカスタマイズできそうです。

update_resourceメソッドの中では、update_with_passwordメソッドが呼ばれていることがわかります。次にこのメソッドを確認します。

また、update_resourceメソッドをオーバーライドするにあたって、参考のため、そのデフォルトの挙動についても確認しておきます。

## Devise::Models::DatabaseAuthenticatable#update_with_passwordの実装

~~~ruby

      # Update record attributes when :current_password matches, otherwise
      # returns error on :current_password.
      #
      # 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.
      def update_with_password(params, *options)
        if options.present?
          ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
            [Devise] The second argument of `DatabaseAuthenticatable#update_with_password`
            (`options`) is deprecated and it will be removed in the next major version.
            It was added to support a feature deprecated in Rails 4, so you can safely remove it
            from your code.
          DEPRECATION
        end

        current_password = params.delete(:current_password)

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

        result = if valid_password?(current_password)
          update(params, *options)
        else
          assign_attributes(params, *options)
          valid?
          errors.add(:current_password, current_password.blank? ? :blank : :invalid)
          false
        end

        clean_up_passwords
        result
      end

~~~
参考:[devise/lib/devise/models/database_authenticatable.rb ](https://github.com/heartcombo/devise/blob/451ff6d49c71e543962d2b29d77f2e744b2d47e1/lib/devise/models/database_authenticatable.rb#L87)

説明書きによると、次のことがわかります。

- :current_passwordに存在した時に要素を更新する。
- :current_passwordに存在しなかった場合は、:current_passwordについて、エラーを発生させ、更新させない。
- password フイールドが空白であった場合も同様に、更新させない。ただし、パスワードの変更を伴わない、e-mailといった情報の変更は、この限りではない。
- password_confirmation フィールドが空白だった場合も同様に更新させない。

具体的に処理の中身を確認します。今回は、引数にオプションがあった場合の処理については、触れません。

上から順番に確認します。
~~~ruby
current_password = params.delete(:current_password)
~~~
ここでは、変数current_passwordにparams.delete(:current_password)の値を代入しています。
#### deleteメソッドについて
key に対応する要素を取り除き、その要素を返します。
参考：[instance method Hash#delete](https://docs.ruby-lang.org/ja/latest/method/Hash/i/delete.html)

つまり、params.delete(:current_password)では、フォームに入力された値を格納したハッシュ（params)
から:current_passwordに紐づく要素を取得していることになります。
次の処理を確認します。

~~~ruby
if params[:password].blank?
    params.delete(:password)
    params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
~~~

params[:password]に紐づく要素が存在しなかった場合は、:passwordと:password_confirmationをキーに持つ要素を取り除いています。

次の処理を確認します。
~~~ruby
result = if valid_password?(current_password)
          update(params, *options)
        else
          assign_attributes(params, *options)
          valid?
          errors.add(:current_password, current_password.blank? ? :blank : :invalid)
          false
        end
~~~
変数current_passwordがパスワードとして妥当であれば、paramsハッシュの内容で情報を更新しています。
妥当でなかった場合は、更新前の要素をassign_attributesメソッドで再度割り当てを行い、エラー原因を条件分岐させて該当するエラーを発生させています。
resultという変数に条件式の結果を代入しています。
参考:[assign_attributes](https://railsdoc.com/page/assign_attributes)
参考:[【Rails】errors.addって何？](https://qiita.com/yujiG/items/3e34e2e0e7b4120b0584)

最後の部分を確認します。

~~~ruby
clean_up_passwords
        result
~~~
clean_up_passwordsはdevise内部で定義されているメソッドで、パスワードの値をクリアしています。

~~~ruby
  # Set password and password confirmation to nil
      def clean_up_passwords
        self.password = self.password_confirmation = nil
      end
~~~
参考：[devise/lib/devise/models/database_authenticatable.rb ](https://github.com/heartcombo/devise/blob/main/lib/devise/models/database_authenticatable.rb#L76)

最後に、変数resultに格納されていた、条件式の結果を返しています。

ここまでで、params[:current_password]の値をdeleteメソッドで取り出して、その値で条件分岐しているということがわかりましたので、実際にメソッドを自分で定義してみます。

## Users::RegistrationsController#update_resource の実装
それでは、実際にメソッドをオーバーライドします。
Controllerクラスのフォルダー構造は次のとおりです。usersフォルダーを作成して、その下にregistrations_controller.rbファイルを作成しています。
![スクリーンショット 2022-02-28 14.38.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/fd34e247-e2ef-636d-ed31-bcb1e19d0e5e.png)


まず、Devise::RegistrationsControllerを継承したUsers::RegistrationsControllerクラスを定義します。
Users::RegistrationsControllerは、users/registrations_controller.rbというファイル構造を表しています。
参考：[定数の自動読み込みと再読み込み (Zeitwerk)](https://railsguides.jp/autoloading_and_reloading_constants.html)
update_resourceメソッドをオーバライドします。メソッド内部でカスタムメソッドのupdate_without_current_passwordメソッドを呼び出す記述を行います。


~~~ruby:users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController

    protected
  
    def update_resource(resource, params)
      resource.update_without_current_password(params)
    end
  end
~~~

それでは、update_without_current_passwordメソッドの処理を記述します。ご覧いただいてわかるかと思いますが、Devise定義のupdate_with_passwordメソッドの処理を少し変えただけです。

### 今回実現したいこと
それでは、今回実現したいことをもう一度再確認します。
実現したい実装は以下の２点でした。

- 現在のパスワード（current_password）の入力なしでユーザー情報を更新したい。
- パスワードの変更の場合は、現在のパスワードの入力を求める。

## RegistrationsController#update_without_current_passwordの実装
~~~ruby:user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :name, presence: true, length: { maximum: 50 }

  def update_without_current_password(params, *options)


    if params[:password].blank?
      params.delete(:password)
      params.delete(:password_confirmation) if params[:password_confirmation].blank?
      params.delete(:current_password)
      result = update(params, *options)
    else
      current_password = params.delete(:current_password)
      result = if valid_password?(current_password)
        update(params, *options)
      else
        assign_attributes(params, *options)
        valid?
        errors.add(:current_password, current_password.blank? ? :blank : :invalid)
        false
      end
    end

    clean_up_passwords
    result
  end
end
~~~
処理としては、次のような感じです。
~~~
if フォームのパスワード欄が空欄？
　　　パラメータのパスワード情報を削除
　　　パラメータのパスワード確認情報を削除
　　　パラメータの現在のパスワード情報を削除
　　　結果=self.update(名前情報,メールアドレス情報)
else #(フォームのパスワード欄に入力が有りの場合)
　　　current_password=パラメータの現在のパスワード情報
　　　result= if 現在のパスワードが正しい？
        結果=self.update(名前情報,メールアドレス情報,パスワード情報,パスワード確認情報)
     else#(フォームの現在のパスワード欄に入力誤りの場合)
        更新中の値（名前情報,メールアドレス情報,パスワード情報,パスワード確認情報）をフォームの各欄に戻す
　　　　　現在のパスワードがエラーになった理由を返す。
　　　end
end
~~~
これでメソッドの記述は以上となります。
最後にルーティングを編集して、Devise::RegistrationsControllerではなく、カスタムしたRegistrationsControllerを参照するようにします。

~~~ruby
Rails.application.routes.draw do
  devise_for :users, 
    controllers: { registrations: 'users/registrations' } 
end
~~~

Deviseのルーティングをカスタマイズするためには、
devise_forで、controllerオプションにハッシュを指定します。
routes.rbでdevise_forと記述することで、deviseがルーティングを設定してくれています。

~~~ruby
devise_for :users, 
    controllers: { Deviseのコントローラー名　: 'カスタムコントローラー名' } 
~~~

> controllers: the controller which should be used. All routes by default points to Devise controllers.
  However, if you want them to point to custom controller, you should do:
>devise_for :users, controllers: { sessions: "users/sessions" }

引用：[Method: ActionDispatch::Routing::Mapper#devise_for](https://www.rubydoc.info/github/heartcombo/devise/master/ActionDispatch/Routing/Mapper%3Adevise_for)

これで、実装完了です！

## まとめ
- Deviseのデフォルトの挙動を変更するためには、オーバーライドが必要。
- デフォルトのソースコードを確認して、できるだけそれを活用すれば、案外簡単に変更が可能。
- カスタムメソッドを参照するとように、routes.rbを編集する必要がある。

## 参考
[heartcombo/devise](https://github.com/heartcombo/devise)
[Method: ActionDispatch::Routing::Mapper#devise_for](https://www.rubydoc.info/github/heartcombo/devise/master/ActionDispatch/Routing/Mapper%3Adevise_for)
[Devise でユーザーがパスワードなしでアカウント情報を変更するのを許可](https://easyramble.com/user-account-update-without-password-on-devise.html)