rails deviceを使用したメールアドレス変更

## はじめに
deviceを使用したメールアドレス変更機能を実装する機会があったので、学んだことをメモに残します
主に、GitHubで公開されているdeviseのコーディングを追う際のメモとしてきさいしていきます

## 実装内容
・メール認証を挟んだメールアドレス変更を実装したい
（今回はメールアドレスを変更できるのはログインしている状態のみ）

処理の流れは以下

新しいメールアドレスを取得
↓
認証URLを含む認証メールの送信
↓
認証できたらメールアドレス変更


## 前提事項
・既にdeviseが導入されている環境
・メール認証を含む新規登録〜ログインまで実装済み
（confirmable機能はすでに実装済み）

## 実装手順
### ①データベースへの項目追加
confirmable機能が利用するデータ項目を追加してあげる必要があります。

メールアドレス変更で使用するカラムは```unconfirmed_email```


```app/db/migrate/20230603034400_add_column_to_users.rb
class AddColumnToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column  :users,  :unconfirmed_email,   :string
  end
end
```
#### unconfirmed_emailについて
変更後メールアドレスを認証完了まで一時的に保持しておくためのカラムです。
認証完了までは、変更前のメールアドレスと使用してログインできる必要があるので、このカラムがあります。
※メール認証が不要の場合はこのカラム自体が不要となります
##### unconfirmed_emailの大まかな流れ
新しいメールアドレスをunconfirmed_emailカラムへ登録する
↓
認証メールURL押された
↓
unconfirmed_emailカラムをemailカラムへコピー
↓
unconfirmed_emailカラムを空に

### ②configファイルの修正

```config/initializer/devise.rb
config.reconfirmable = true
```
```reconfirmable = true```にすると、メールアドレス変更時に認証メールが送られるようになります。
※メールアドレス変更時のメール認証が不要であれば、```reconfirmable = false```にする必要があります。
 
### ③更新用controllerを作成
```【ざっくり】app/controllers/devise/registrations_controller.rb
prepend_before_action :authenticate_scope!, only: [:update] 
before_action :configure_account_update_params, only: [:update]

def update
  resource.attributes = account_update_params

  if resource.save 
    #ここでメール認証のメソッドが実行される
    respond_with resource, location: after_update_path_for(resource)
  else
    # エラー処理
  end

  protected
  # パラメーター制御のためにオーバーライドしてます（emailだけが送られてくる想定）
  # override Devise::RegistrationsController#configure_account_update_params
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:email])
  end
end
```
##### パラメーターについて
edit画面のinputタグには、unconfirmed_emailをではなくemailを指定することに注意してください。
上記の理由から```configure_account_update_params```でemailだけに絞っています。

##### prepend_before_action :authenticate_scope!, only: [:update] 
簡単に言うとログイン情報をもとにresourceへデータを格納している処理です。
参考：[devise/app/controllers/devise/registrations_controller.rb](https://github.com/heartcombo/devise/blob/4b72064bfcf076478c5c87818b9536b203f6584f/app/controllers/devise/registrations_controller.rb#LL130C3-L134C6)



### ④認証メール送信処理作成
ユーザーが既存のアカウントでメールアドレスを変更した直後、以下のメソッドが呼び出されます。

```【ざっくり】models/confirmable.rb
def send_confirmation_instructions
  unless @raw_confirmation_token
    generate_confirmation_token!
  end
  #認証メール送信処理　（新規登録時の認証メール承認と同様のものを使用したので今回は省略）
  end
end
```
### 〜〜メール認証URLをクリック〜〜

### ⑤unconfirmed_emailカラムをemailカラムへコピー・unconfirmed_emailカラムを空に
メール認証URLを押した直後に実行するのはconfirmations_controller.rbのshowアクションです。

```【devise抜粋】app/controllers/devise/confirmations_controller.rb
def show
    self.resource = resource_class.confirm_by_token(params[:confirmation_token])

    yield resource if block_given?

    if resource.errors.empty?
      #メールアドレス変更後の処理
    else
      #エラー処理
    end
  end
```
```confirm_by_token```メソッドを呼び出します。

```【devise抜粋】models/confirmable.rb
def confirm_by_token(confirmation_token)
  #deviseのコードそのまま使用するので省略

  confirmable.confirm if confirmable.persisted?
  confirmable
end
```
confirmメソッドの中でunconfirmed_emailカラムをemailカラムへコピー、unconfirmed_emailカラムを空にする処理をしています。

```【devise抜粋】models/confirmable.rb
def confirm(args = {})
  pending_any_confirmation do
    if confirmation_period_expired?
      self.errors.add(:email, :confirmation_period_expired,
        period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago))
        return false
      end

      self.confirmed_at = Time.now.utc

      saved = if pending_reconfirmation?
        skip_reconfirmation!
        self.email = unconfirmed_email
        self.unconfirmed_email = nil

         # We need to validate in such cases to enforce e-mail uniqueness
         save(validate: true)
    else
      save(validate: args[:ensure_valid] == true)
    end

    after_confirmation if saved
    saved
  end
end
```


## 認証スキップ機能
今回はメール認証込みのメールアドレス変更でしたが、メール認証をスキップすることもできます。
めーる認証をスキップさせるためには、
登録時```skip_confirmation!```メソッド
更新時```skip_reconfirmation!```メソッド
が用意されています。
これらのメソッドを``save``前に呼ぶことで認証メールの送信から全てスキップすることが可能です。
※create,save,updateの**前**に呼び出す必要があります

```【devise抜粋】devise/lib/devise/models/confirmable.rb
#登録時のスキップ機能
def skip_confirmation!
  self.confirmed_at = Time.now.utc
end

#更新時のスキップ機能
def skip_reconfirmation!
  @bypass_confirmation_postpone = true
end
```

上記でも記載している```devise/lib/devise/models/confirmable.rb```のconfirmメソッドでも使用されています。
（confirmメソッド内ではもう認証メール送信後の処理なのでスキップしている）

## まとめ
deviceはGitHubで公開されているdeviseのコーディング内容を実際に取り込んで動かしてみるのが一番の近道でした。

## 参考
[【Rails】deviseでURL認証付きのメールを送信してみる](https://qiita.com/ozackiee/items/21fcad4a1564136b9510)