Railsでdeviseを使用してプロフィール編集ページとは別でパスワード編集ページを自作する方法

## 記事の目的
- Railsでdeviseでプロフィール編集ページとは別でパスワード編集ページを自作した時に、同じようなことをしている記事が見つからず実装に手間取ったので、備忘録として残す
- 今後同じことをしたい人が現れた時に、少しでも参考になるように
- まだまだ勉強中の身です。もし間違っている箇所があれば教えてください🙇‍♂️

## 前提条件
- Rails 6.0.2.2
- Ruby 2.7.0
- deviseを導入している


## 実現したかったこと
- deciseでデフォルトで設定されている`registration#edit`、`registration#update`では名前、メールアドレスなどのプロフィール情報を編集する
- プロフィール編集時はパスワード入力なしで編集できるようにする
- 上記とは別でパスワード編集用のページを自作する
- 現在のパスワード、新しい用パスワード、確認用パスワードを入力するフィールドがあり、入力内容に不備があればパスワード編集ページにリダイレクトし、エラーを出す

## やったこと
- ルーティングの設定
- 対応するviewの作成
- 実現したい内容にあわせてregistration_controllerでdeviseをオーバーライド

## まずは`roures.rb`に`devise_scope`でルーティングを設定
`edit_password` と `update＿password` どちらも書きます。
deviseはなぜかデフォで `user/edit` でアクセスできてしまう（:idがない）が、それだと気持ち悪いので自作したページにはちゃんと:idを足します。
 
```roures.rb
  devise_scope :user do
    get 'registrations/:id/edit_password', to: 'registrations#edit_password', as: 'edit_password'
    put 'registrations/:id/update_password', to: 'registrations#update_password', as: 'update_password'
  end
```

## パスワード編集ページの作成
パスワード編集用のviewを作成 。
中身はデフォで作られているプロフィール編集ページのものをベースにして書き換えればOKです。
`form_for`タグの`url`に、↑で設定した更新時に使用するアクション（今回だと`update_password`）のpathを指定します。

```edit_password.html.slim
.password-edit
  h2 パスワード編集
  = form_for(resource, as: resource_name, url: update_password_path, html: { method: :put }) do |f|
    = render 'devise/shared/error_messages', resource: resource
    .form
      .field
        = f.label :current_password, '現在のパスワード'
        br
        = f.password_field :current_password, autocomplete: 'current-password', class: 'input-box'
      .field
        = f.label :password, '新しいパスワード'
        br
        = f.password_field :password, autocomplete: 'new-password', class: 'input-box', placeholder: '6文字以上'
      .field
        = f.label :password_confirmation, '新しいパスワード（確認）'
        br
        = f.password_field :password_confirmation, autocomplete: 'new-password', class: 'input-box', placeholder: '6文字以上'
      .actions
        = f.submit '更新する', class: 'submit-box'
```

この状態ではまだローカルからこのviewにアクセスできないと思います。（ArgumentErrorが出る）
理由は、`edit_password`は自作したもののためdeviseをうまく経由できておらず、`resource`がviewに渡されていないから。
この問題をdeviseの内容をオーバーライドすることで解決していきます。

## `registration_controller`で`devise`をオーバーライドしていく（ここが大変だった..！）
まずは↑の状態を解消するために、`devise`の`before_action`をオーバーライドしていきます。
devise本体のコードは以下。

``` devise/registrations_controller.rb 
prepend_before_action :authenticate_scope!, only: [:edit, :update, :destroy] 
prepend_before_action :set_minimum_password_length, only: [:new, :edit]
```
詳細は本体のコードを読んで欲しいですが、この`authenticate_scope!`が現在の`resource`を渡すアクションのため、ここに`edit_password`と`update_password`を追加します
（その下はパスワードの最小文字数をセットするもの。ついでに追加します）

```registration_controller.rb
class RegistrationsController < Devise::RegistrationsController
  prepend_before_action :authenticate_scope!, only: [:edit, :edit_password, :update, :update_password, :destroy]
  prepend_before_action :set_minimum_password_length, only: [:new, :edit, :edit_password]
```
これで無事にパスワード編集ページにアクセスできるようになりました！
次は`update_password`の条件を書いていきます。
devise本体の`update`をベースにすればOKですが、そのままだと更新失敗時にプロフィール編集ページにリダイレクトされてしまうため、パスワード編集ページにリダイレクトされるように書き換えます。


```registrations_controller.rb 

def edit_password; end

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

    resource_updated = update_resource(resource, account_update_params)
    yield resource if block_given?
    if resource_updated
      set_flash_message_for_update(resource, prev_unconfirmed_email)
      bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?

      respond_with resource, location: after_update_path_for(resource)
    else
      clean_up_passwords resource
      set_minimum_password_length
      #この1行を書き換えた
      render 'edit_password'
    end
  end
```

次に、実際に更新を行っている `update_resource`アクションを条件に合わせてオーバーライドします。
デフォルトはこんな感じ。

```devise/registrations_controller.rb 
  def update_resource(resource, params)
    resource.update_with_password(params)
  end
```

ここでやりたいことを改めて振り返ると、プロフィールを編集する時はパスワードなしで更新できるようにし、パスワードを編集する時だけパスワード入力を求めるようにしたいです。
しかしこのままだとプロフィールを編集する時もパスワード入力を求められてしまうので、以下のようにオーバーライドします。

```registrations_controller.rb 
  protected

  def update_resource(resource, params)
    if params[:password].blank? && params[:password_confirmation].blank? && params[:current_password].blank?
      resource.update_without_password(params)
    else
      resource.update_with_password(params)
    end
  end
```
これでちゃんと動くようになった！
最後に、無事に更新できた時のリダイレクト先を設定します。
デフォルトは`root_path`になっていますが、ここをユーザー詳細ページにしたいため、`after_update_path_for` をオーバーライドします。

```registrations_controller.rb 
  protected

  def after_update_path_for(resource)
    user_path(current_user)
  end
```
以上！

## まとめ
gemはとても便利ですが、カスタマイズしたい時は、本体のコードをよく理解する必要があって大変だなと痛感..！！
でも無事に解決できて良かったです。最後までご覧いただきありがとうございました。