【devise】アカウント情報変更機能実装

こんばんは。
現在オートバイのレビューサイトを作成中のプログラミング初学者です。
マイページに、アカウント編集機能を実装するにあたって、いくつか壁にぶち当たったので、実装までの流れをまとめたいと考え、投稿します。

# 目的
##### deviseを用いて、マイページ内にアカウント情報編集機能を実装する

# 環境
Ruby on rails  ver.6.1.3.1
Ruby           ver.2.6.5
Docker         ver.20.10.5
Docker compose ver.1.28.5

# 前提条件
・gem Devise導入済み　[公式Github](https://github.com/heartcombo/devise)
・マイページ実装済み(参考：私はusersコントローラーを自作し、showアクションにてマイページを実装しました。)

# 実装

#### ①ルーティング設定
```ruby
 devise_for :users, controllers: { registrations: 'users/registrations' }
```
devise導入で、すでに`devise_for :users`までは記載済みかと思いますが、
controllersオプションを追記し、deviseで標準装備されているregistrationsコントローラーを用いてアカウント情報編集機能を実装していきます。
`rails routes`でPrefixパスの確認などをしておいてもいいかと思います。

#### ②application_controller.rb編集
```ruby
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
    devise_parameter_sanitizer.permit(:account_update, keys: [:nickname])
  end
end
```
ストロングパラメーターを実装します。
deviseを導入済みであれば、アカウント新規登録時に用いるストロングパラメーター`configure_permitted_parametersメソッド`(メソッド名は任意、慣習的に使用。)を実装済みかと思いますが、アカウント情報編集のために`devise_parameter_sanitizer.permit(:account_update, keys: [:nickname])`を追記します。
※アカウント情報にemail、password以外のカラムを用意している場合。私は`nicknameカラム`を追加しています。

#### ③ビューファイルにアカウント情報変更画面への遷移パスを設置
```erb
  <div class="user-edit">
    <% if user_signed_in? && @user == current_user %>
        <%= link_to "アカウント情報編集", edit_user_registration_path, class: "user-edit-btn" %>
    <% end %>
  </div>
```
classなどは自由ですが、`if user_signed_in? && @user == current_user`で*ユーザーとしてサインイン済みであること*、*遷移しようとするアカウント情報変更画面は今ログインしているユーザーであること*を前提にリンクを設置します。
※`インスタンス変数user`は事前にusersコントローラーのshowメソッド内で、`@user = User.find(params[:id])`などと定義しておきましょう。

#### ④アカウント情報変更画面の実装
```erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div class="field">
    <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
    <% if @minimum_password_length %>
      <br />
      <em><%= @minimum_password_length %> characters minimum</em>
    <% end %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
    <%= f.password_field :current_password, autocomplete: "current-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>

<%= link_to "Back", :back %>
```
ビューファイルは`views/devise/registrations/edit.html.erb`を使います。
初期は上記のように必要十分の装備と、味気ない見た目となりますので、これを編集していきます。

```erb
<div class="user-edit-contents">
  <h1>プロフィール編集</h1>
    <div class="user-edit-form">
    <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
      <%= render "devise/shared/error_messages", resource: resource %>

      <div class="field">
        <%= f.label :email %>
        <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
      </div>
      <div class="field">
        <%= f.label :nickname %>
        <%= f.text_field :nickname, autofocus: true, autocomplete: "nickname" %>
      </div>

      <div class="field">
        <%= f.label :password %>
        <%= f.password_field :password, autocomplete: "new-password" %>
      </div>

      <div class="field">
        <%= f.label :password_confirmation %>
        <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
      </div>

      <div class="actions">
        <%= f.submit "保存", class:"user-update-btn" %>
      </div>
    <% end %>

    <%= link_to "戻る", user_path(current_user.id), class:"redirect-btn" %>
  </div>
</div>
```
編集後がこちらになります。昼下がりの料理番組のようですね。
ポイントは下記の通りです。
①メール認証部分を削除
②nickname変更フォームを追加
③パスワード最小文字表示部分の削除
④:current_passwordフォーム削除(後ほど解説します)
⑤アカウント削除ボタン削除

このままでは、正しくフォームを入力しても更新されませんし、エラーが出ることがあります。

#### ⑤registrations_controller.rb編集
deviseで標準装備されているコントローラー`controllers/users/registrations_controller.rb`に必要なメソッドを定義していきます。
※標準ではコメントだらけのコントローラーファイルになっているかと思います。

```ruby
  protected #コメントアウト外す

  def update_resource(resource, params)
    resource.update_without_current_password(params) #独自のメソッド。解説は下記にて。
  end

  #更新後のパスを指定。マイページに戻るように設定。
  def after_update_path_for(resource)
    user_path(@user.id)
  end
```

##### update_without_current_passwordメソッドについて
deviseでアカウント情報を変更する際に、`現在のパスワード`が必須となります。
毎回パスワードを入力しないといけないのは面倒なので、省くメソッドを作ります。
deviseに標準であるメソッドは`update_without_password`であり、アカウント情報変更時に`パスワードそのもの`を必要としなくすることは可能ですが、
今回は`現在のパスワード`は不要でも、`パスワードの変更`は可能とするように実装していきます。

#### ⑥Userモデルにupdate_without_current_passswordメソッドの定義
```ruby
  def update_without_current_password(params, *options)
    params.delete(:current_password)
    
    if params[:password].blank? && params[:password_confirmation].blank? 
      params.delete(:password)
      params.delete(:password_confirmation)
    end
    
    result = update(params, *options)
    clean_up_passwords
    result
  end
```
これにて`現在のパスワード`はパラメーター内で不要となり、かつ`パスワード`と`パスワードの確認`を空とした場合、パスワードの更新はなされなくなります。
参考にした記事の多くは`update_attributesメソッド`を使っていましたが、おそらくrailsの6.1以降はこのエイリアスの使用はできずNoMethodErrorを返しますので、同義の`updateメソッド`を採用します。

##### これで実装完了です。お疲れ様でした。
[![Image from Gyazo](https://i.gyazo.com/460f91e27915efd428264be028d25144.png)](https://gyazo.com/460f91e27915efd428264be028d25144)

※上記例はdeviseの日本語化をしています。CSSはご自由に。

# 補足
## 正規表現のバリデーションを設けている場合
`userモデル`で`PASSWORD_REGEX`のようなバリデーションを設けている場合、これが反応してどれだけパスワードを入力しても、バリデーションに引っかかるエラーが出現するかと思います。(実際私はこれにかなり苦戦しました。)

```user.rb
PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX, 
    message: 'には英字と数字の両方を含めて設定してください'
```
この場合、このバリデーションに下記1点メソッドを追記してください。

```user.rb
  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX, 
    message: 'には英字と数字の両方を含めて設定してください', if: :password_required?
```
deviseの標準メソッドのうちの1つです。　参考：[varidatavle.rb](https://github.com/heartcombo/devise/blob/v4.8.0/lib/devise/models/validatable.rb)
これによりpasswordの有無を判定します。その上で、正規表現バリデーションを適応させます。

## ゲストログイン機能を実装している場合
ポートフォリオでよくあるゲストログイン。ワンタップでログインでき、アプリの機能を試せるのは魅力的ですよね。
今回アカウント情報編集機能の実装ができましたが、ゲストユーザーに限っては誰でもログインできる特性上、簡単に編集できるのは望ましくありません。
コントローラーに壁を張ります。

```registrations_controller.rb
 before_action :ensure_normal_user, only: [:edit, :update] #メソッド名は任意

protected

  #ゲストユーザーはアカウント情報を編集・更新できない
  def ensure_normal_user
    if resource.email == 'guest@example.com'
      redirect_to root_path, alert: 'ゲストユーザーは編集できません。'
    end
  end
```

ゲストユーザーではないことを確認するメソッドを追加しました。
ゲストログインに使用されるメールアドレス(今回はguest@example.com)でeditアクション、updateアクションを実行しようとした場合、ルートパスにリダイレクトさせ、アラートを表示させるようにしました。
これで、ゲストユーザーはそもそもアカウント情報編集画面にすら遷移できなくなります。

# まとめ
ここまでご覧いただきありがとうございました。
何か間違いなどがあれば遠慮なくご指摘いただけますと幸いです。

# 参考記事
[公式Github](https://github.com/heartcombo/devise)
[[*Rails*] deviseの使い方（rails5版）](https://qiita.com/cigalecigales/items/f4274088f20832252374)
[devise ユーザーのプロフィール画面作成と編集（デフォルトをカスタマイズ）](https://qiita.com/akr03xxx/items/82ba45f7ef4fdbd5c702)
[deviseで現在のパスワード無しでuserを更新する](https://docs.komagata.org/4911)