【Rails】deviseでURL認証付きのメールを送信してみる

#はじめに

某プログラミングスクールでのチーム開発振り返り編。
フリーマーケットWebアプリ（コピー）を作った際、本家に以下のような機能がありました。

**【実現したいこと】**
・ ユーザ登録：Welcomeメッセージが届く（認証URLなし）
・ メールアドレス変更：変更前アドレスに認証メールが届く

よっしゃ、やってやろー٩( 'ω' )و
と言う事で、deviseの”confirmable"と言う機能でサクッ(?)と試してみた時の振り返りメモです。

間違っている点や、コードの改善点、こんな機能で代替できるよ、などあれば是非ご指摘・指南いただければ嬉しいです！よろしくお願いいたします！！

#前提事項
既にdeviseが導入されている環境に対して、改めて機能追加することを前提としています。

【動作環境】
　テンプレートエンジン：erb→haml変換して利用しています
　データベース：MySQL

【事前に完了させておくべき作業】
　devise (4.6.2)
　① gemにdeviseが導入されて``bundle install``が完了している
　② devise用のコントローラー及びモデルの作成が完了している
　③ deviseが提供してくれるviewを作成完了している
　④ メッセージなどの日本語化が完了している
　⑤ ルーティングの設定が完了している（これは自動かな）

　※私はUserと言うモデルでdevise機能を構築しています。
 　違う場合は、適宜読み替えてください。

#手順の大まかな流れ
以下の流れで説明していきたいと思います。

[confirmable機能の有効化]
　↓
[データベースへの項目追加]
　↓
[Controllerのカスタマイズ]
　↓
[送信メールフォームの修正]※途中からMailerのカスタマイズへの分岐あり
　↓
[confirmable機能のオプション指定]
　↓
[メール送信設定]※別Qiitaに飛びます
　↓
[認証メールの動作確認]

[途中導入の注意点]※おまけ的な

#手順の説明
それではチャキチャキはじめます。

##confirmable機能の有効化
deviseの初期導入時点では「confirmable」の機能は有効化されていません。
そのため、設定後のように末尾に「:confirmable」を追加してください。
※カンマ区切りもお忘れなく

```app/models/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, :confirmable #←ココに追加だけ
  #以下略#
end
```

##データベースへの項目追加
confirmable機能が利用するデータ項目を追加してあげる必要があります。
初期のマイグレーションファイルであれば該当行のコメントを外すのみですが、後から追加する場合は改めてマイグレーションファイル作成から始める必要があります。

###①マイグレーションファイル作成
コンソール画面で該当のプロジェクトフォルダに移動し、以下のコマンドを実行します。このコマンドを実行することでマイグレーションファイルと言うDB操作用のファイルが自動作成されます。

```console.
   $ rails g migration AddColumnToUsers　←このコマンドを実行
   # 実行結果
     Running via Spring preloader in process 29552
      invoke  active_record
      create  db/migrate/20190917034400_add_column_to_users.rb
```
※``AddColumnToUsers``部分はぶっちゃけ、何を書いてもエラーにはなりません。
　ただ、お作法があるみたいなので、詳しくはググってみてください！ざっくりとは↓な感じ！
　[実行したい操作(AddとかRemoveとか)]＋[対象のカラム名]+[ToとかFromとか]+[テーブル名]

###②マイグレーションファイルの編集
①で作成されたマイグレーションファイルを開き、追加したい項目を定義します。

```app/db/migrate/20190917034400_add_column_to_users.rb
class AddColumnToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column  :users,  :confirmation_token,  :string
    add_column  :users,  :confirmed_at,        :datetime
    add_column  :users,  :confirmation_sent_at,:datetime	
    add_column  :users,  :unconfirmed_email,   :string       #email変更時の認証が不要であれば、この項目は必要ではありません。ただし、configの「reconfirmable」を「false」にする必要があります。
  end
end
```
**【補足説明】それぞれの項目説明**
今回追加した４つの項目について、どのような目的のものか少し補足します。

| カラム名              | 説明               |入力値例            |
|:-----------------   |:------------------|:------------------|
| confirmation_token  |一意のランダムトークン。この値を使って認証します|KtMVn65jFxDT1qB6aY5P|
| confirmed_at        |ユーザーが確認リンクをクリックしたときのタイムスタンプ|2019-09-17 13:12:37|
| confirmation_sent_at|confirm_tokenが生成されたときのタイムスタンプ|2019-09-17 13:06:18|
| unconfirmed_email   |変更後メールアドレスを認証完了まで一時的に保持。確認リンクが押された時点でemailが上書きされ、このカラムは空に（メールアドレス変更時のみ使用）|xxxxx@gmail.com|
【参考】[GitHub:devise/confimation.rbのコメント](https://github.com/plataformatec/devise/blob/master/lib/devise/models/confirmable.rb)
###③マイグレーションの実行
①、②で作成したマイグレーションファイルをDBに適用させます。
再度コンソール画面からプロジェクトフォルダに移動し、以下のコマンドを実行してください。

```console.
$ rails db:migrate ←このコマンドを実行
# 実行結果
== 20190917034400 AddColumnToUsers: migrating =================================
-- add_column(:users, :confirmation_token, :string)
   -> 0.1849s
-- add_column(:users, :confirmed_at, :datetime)
   -> 0.0692s
-- add_column(:users, :confirmation_sent_at, :datetime)
   -> 0.1772s
-- add_column(:users, :unconfirmed_email, :string)
   -> 0.1500s
== 20190917034400 AddColumnToUsers: migrated (0.5817s) ========================
```
##Controllerのカスタマイズ
今回ユーザ登録時に認証不要なので、devise機能をカスタマイズして対応します。

ただし、ユーザ登録及び、更新のタイミングで認証メールは**カスタマイズしなくても送信されます**。
不要な方は、この手順自体をスキップしてください。
 
###①オーバーライドする
ユーザ登録時は、認証メールのスキップを行いたいので、deviseコントローラのcreateアクションのみオーバーライドしちゃいます。手動で``registrations_controller.rb``を作成してください。

```app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController #deviseの該当クラスを継承させる
  def create
    super do                                             # 他はdeviseの機能をそのまま流用する
      resource.update(confirmed_at: Time .now.utc)       # Welcomeメールを送信した上で、skip_confirmation!と同一処理を行い自動で認証クローズさせる
      #↓と同じ意味になります。
      # resource.skip_confirmation!
      # resource.save
    end
  end
end
```
deviseは認証済みかどうかの判断を``confirmed_at``に日付が入っているかどうかで判定しているようです。そのため、confirmable機能でメールを送信した上で、``confirmed_at``に値を入れて自己完結させてる感じです。

####【補足】認証スキップ機能
ユーザ登録時の認証をスキップさせるためには、実は専用の``skip_confirmation!``メソッドが用意されています。このメソッドを``save``前に呼ぶことで**認証メールの送信ごと**スキップすることが可能です。
なお、登録時と変更時で呼び出すメソッドが異なるようです。ご注意を。

| メソッド名              | 説明               |
|:-----------------   |:------------------|
|skip_confimation!|ユーザ登録時の認証メールをスキップさせる|
|skip_reconfirmation！|email変更時の認証メールをスキップさせる|
※いずれもcreate,save,updateの前に呼び出す必要があります
※仮に認証スキップ後に任意のメッセージを出す場合は、独自でActionMailerを定義し、呼び出す必要があります。

```【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
```
####【補足】コーディングの意図
何故、今回この機能を使わなかったか？
少しご説明しておくと、スキップ機能が``save``前と言及した所にあります。
今回私は、コーディングを面倒くさがって、deviceのcreateアクション内の差し込み定義位置（``yield resource if block_given?``）に処理を追加しました。
下図が実際のdeviseのコード抜粋版です。

```【devise抜粋】devise/app/controllers/devise/registrations_controller.rb
def create
  build_resource(sign_up_params)
    resource.save
      yield resource if block_given?    # ← ココに処理を差し込める!
      if resource.persisted?
       if resource.active_for_authentication?
         set_flash_message! :notice, :signed_up
         sign_up(resource_name, resource)
         respond_with resource, location: after_sign_up_path_for(resource)
       else
         set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
         expire_data_after_sign_in!
          respond_with resource, location: after_inactive_sign_up_path_for(resource)
       end
     else
       clean_up_passwords resource
       set_minimum_password_length
       respond_with resource
     end
   end
```
ご覧の通り、差し込む場所がsaveより後なので、差し込んでもスキップされません！笑
そのほかに、deviseが提供しているメールフォームをそのまま利用したかった。という理由もあり、上記のコードになりました。多分もっとスマートなやり方があると思います^^;

**【参考】``yield resource if block_given?``とは？**
と疑問に思った方。[こちら](https://qiita.com/k-shogo/items/5bbc23e1d0dd0ad3a8a2#super-do-%E3%81%A7%E6%93%8D%E4%BD%9C%E3%82%92%E5%B7%AE%E3%81%97%E8%BE%BC%E3%82%80)が参考になりました！！（もっと早く知っておけば笑）

###②ルーティングの編集
さて、折角オーバーライドさせるために作ったControllerですが、ルーティングを切り替えてあげないと、見に行ってすらくれません。
そのため、以下のようにルーティングを修正しましょう〜。

```config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {       # ← 恐らく最初は”devise_for:”のみの記載かと
    registrations: "users/registrations"
  }
#以下略#
end
```
【参考】[GitHub:devise:readme：コントローラーの構成](https://github.com/plataformatec/devise/#configuring-controllers)
##送信メールフォームの修正
conrimable機能用にdeviseが標準提供してくれているファイルが存在します。
自分たち向けにカスタマイズしていきましょう。
※「ファイルが無い！」と言う方は、devise導入時に``rails g devise:views users``を実行されていないものと思われます。
###①送信元メールアドレスの修正
deviseから送信されるメールの送信元メールアドレスはconfigに設定箇所があります。
任意のアドレスに修正してください。

```config/initilizers/devise.rb
  # ==> Mailer Configuration
  # Configure the e-mail address which will be shown in Devise::Mailer,
  # note that it will be overwritten if you use your own mailer class
  # with default "from" parameter.
  config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' #　← これを編集！！！！
```
※ この設定の下にActionMailerの設定変更用のパラメータもあったりします
###②件名（Subject)を編集する
送信メールのタイトルはデフォルトでは「アカウントの有効化について」となっています。
変更したい場合は``devise.ja.yml``を編集してください。

```config/locales/devise.ja.yml
ja:
  devise:
    confirmations:
    #中略#
    mailer:
      confirmation_instructions:
        subject: 'アカウントの有効化について'  # ←　コチラ !!!
```

って、あ、タイトル登録と編集で切り替えできなくないかコレ・・・

ちっ、畜生ぉおおおーーーーー(ΩωΩ)！！！
と言うことで、それじゃあ気が済まない人は下記の手順をどうぞ。

####②-1 デフォルトのMailer設定を変更する
Mailerがオーバーライドできるようにconfigにちゃんと設定があったりします。
オーバーライドさせたいクラスのMailerを指定しましょう。

```config/initializers/devise.rb
  # Configure the class responsible to send e-mails.
  # config.mailer = 'Devise::Mailer'
  config.mailer = 'Users::Mailer'  # ←　こんな感じで設定追加
```
####②-2 Mailerをオーバーライドする
早速オーバーライドさせましょう。以下のような感じでどうぞ。

```app/mailers/users/mailer.rb
class Users::Mailer < Devise::Mailer
  helper :application
  include Devise::Controllers::UrlHelpers
  default template_path: 'devise/mailer'
  def confirmation_instructions(record, token, opts={})
    #record内にユーザ情報が入っていました。そこの"unconfirmed_email"の有無で登録／変更を仕分けます
    #opts属性を上書きすることで、Subjectやfromなどのヘッダー情報を変更することが可能！
    if record.unconfirmed_email != nil
      opts[:subject] = "【●●●アプリ】メールアドレス変更手続きを完了してください"
    else
      opts[:subject] = "【●●●アプリ】会員登録完了"
    end
    #件名の指定以外は親を継承
    super
  end
end
```
【参考】[GitHub：devise：wiki](https://github.com/plataformatec/devise/wiki/How-To:-Use-custom-mailer)

コレで件名も分岐できましたね。やった。
片方は``devise.ja.yml``使うと言う手もありかもしれません。

**【補足】指定可能なヘッダー情報**

| ヘッダー名| 概要|
|:-----------------|:------------------|
|to,cc,bcc|宛先、写し、ブラインドカーボンコピー|
|subject|件名|
|from|メールの送信元|
|date|メールの送信日時|
|reply_to|返信先のメールアドレス|
|x_priority/x_msmail_priority|メールの重要度|
|content_type|コンテンツタイプ（デフォルトはtext/plain)|
|charset|使用する文字コード(デフォルトはUTF-8)|
|parts_order|複数形式を挿入する順番（デフォルトは["text/plain","text/enriched","text/html"]|
|mime_version|MIMEのバージョン|

###③送信メールフォームを編集する
登録時／更新時で同じメールフォームが呼び出されます。
そのため、前項と同様に``unconfirmed_email``の値があるかどうかで、呼び出し元が登録か変更かを判断し、フォーム内容を切り分けています。内容は適当です！笑

```app/views/devise/mailer/confirmation_instructions.html.haml
//登録／変更かを「unconfirmed_email」の有無で切り分けてメール送信させます。
%p
  ="#{@resource.name}さん"   #　これは私が独自に追加した項目です
  %br
  ●●●をご利用いただきありがとうございます。
  %br
  %br
- if @resource.try(:unconfirmed_email?)  #true→email変更 false→会員登録
  %p
    以下のURLをクリックして、メールアドレス変更手続きを完了してください。
    %br
    = link_to "#{@resource.unconfirmed_email}", confirmation_url(@resource, confirmation_token: @token)
    %br
    %br
    このURLの有効期限は24時間です。
-else
  %br
  会員登録が完了しました。
%br
%br
%p
  Webページを開く
  %br
    http://localhost:3000 //適宜修正ください
  %br
  ※このメールは返信しても届きません。お問い合わせはアプリを起動して「お問い合わせ」からお願いいたします。
  %br
  %br
  ■ご登録の覚えがないのにこのメールが届いたという方
  %br
  ご迷惑をおかけし申し訳ありません。大変お手数ですが下記メールアドレスまでご連絡をお願いいたします。
  %br
  ******@****.jp
```

##オプションを指定する
deviseには便利なオプションがあります。
今回は、認証メールの有効期限を24時間以内に設定するために``confirm_within``オプションを設定します。

```config/initializers/devise.rb
  # A period that the user is allowed to confirm their account before their
  # token becomes invalid. For example, if set to 3.days, the user can confirm
  # their account within 3 days after the mail was sent, but on the fourth day
  # their account can't be confirmed with the token any more.
  # Default is nil, meaning there is no restriction on how long a user can take
  # before confirming their account.
  config.confirm_within = 1.days          # ← コメントアウトを解除して1.daysに。
```
※他のオプションも前後に記載があります。

####【補足】期限が切れた場合の動き
deviseでは「認証期限が切れた」場合に表示する専用の画面が用意されています。
ここで「変更後のメールアドレス」を指定し、ボタンを押すと再度認証メールを送信することが可能です！
なので、必要に応じてコチラも画面のカスタマイズをしておくことをオススメします！
因みにこんな感じで動いてました。

　・期限が切れた場合　　　　　　　　　　→　期限切れであるエラーが出た上で、ボタン押下でメール再送
　・同一認証メールから２回認証URLクリック　→　既に登録済みであるエラーが出て、ボタン押しても何も起きない

```app/views/devise/confirmations/new.html.haml
%h2 Resend confirmation instructions
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email)
  .actions
    = f.submit "Resend confirmation instructions"
= render "devise/shared/links"


```

**【補足】confirmable関連のオプション**

| オプション名              | 説明               |入力例            |
|:-----------------   |:------------------|:------------------|
| allow_unconfirmed_access_for  |認証前でも新アドレスでWebアプリケーションにアクセスできるようにしたい場合、この期間を指定します。デフォルトは0日になっており、必ず確認が必要です。<br>※使い道が個人的に不明‥|config.allow_unconfirmed_access_for = 2.days|
| reconfirmable  |メールアドレス変更時にも認証メールを出す場合、trueにします。デフォルトでtrueになっています。（登録時の認証メールと同一のメールが飛びます）※この項目をtrueにする場合、必ずDBに”unconfirmed_email"が必要です|config.reconfirmable = true|
| confirm_within  |送信された確認トークンが無効になるまでの時間。これを使用して、設定された期間内にユーザーに強制的に確認させることができます。デフォルトでは無期限になっています。|config.confirm_within = 3.days|
【参考】[GitHub:devise/confimation.rbのコメント](https://github.com/plataformatec/devise/blob/master/lib/devise/models/confirmable.rb)
##メール送信設定
今回は、お試しとして「letter_opner_web」というgemを使って確認しています。
設定方法については、下記Qiitaにまとめていますので、こちらを参照してくださいませm()m

[【Rails】ActionMailer実装時にLetterOpnerWebが非常に便利だった。](https://qiita.com/ozackiee/items/f4d4c9b6777d9bb13ed7)
##認証メールの動作確認
特に問題なく設定が完了したら``rails s``して、実際にユーザ登録・メールアドレス変更をしてみましょう。下記開発環境でのデフォルトアドレスです。
###①ユーザ登録
http://localhost:3000/users/sign_up
###②メールアドレス変更
http://localhost:3000/users/edit
※ログイン後に実施
###③letter_opner_webでメール内容確認
「letter_opner_web」の設定が完了していれば、下記のアドレスからメール確認が可能です。
http://localhost:3000/letter_opener

**ユーザ登録時のメール**
認証メールにはリンクはなく、認証などに関係なくログイン可能です。
（``after_sign_up_path_for``をオーバーライドして自動ログインしてあげた方が親切かな・・・）
[![Image from Gyazo](https://i.gyazo.com/a18bf1dadf695fc9a7398a77db997dcb.png)](https://gyazo.com/a18bf1dadf695fc9a7398a77db997dcb)
**メールアドレス変更時のメール**
メールアドレスに記載のリンクをクリックすると変更が確定します。
※認証前は、ログインしようと思っても失敗します
[![Image from Gyazo](https://i.gyazo.com/09b21fc141f20c852df7079492f5aa47.png)](https://gyazo.com/09b21fc141f20c852df7079492f5aa47)
とりあえず、こんな感じで実装OKとします。

##途中導入の注意点
confirmable機能を途中から導入している場合、既存ユーザでログイン時にエラーが発生します。これは追加した``conrimed_at``が``null``のために起こります。
既にユーザが存在している場合は、適宜``confirmed_at``の値を埋めてあげてください。

#積み残し／課題
・毎回書き手の積み残しに残っていくテスト・・・
　動作確認のところをキッチリしたテストとして昇華・・・させるぞ・・・来月（え

#まとめ
色々とQiitaを探すよりGitHubで公開されているdeviseのコーディング内容や、コメント、wikiをみた方が色々勉強になるような気がしました。。かなり詳しく書かれているんですね。
もそっとGitHubを見るようにしなきゃだな、と感じました。

#参考
[GitHub:plataformatec/devise](https://github.com/plataformatec/devise/blob/master/lib/devise/models/confirmable.rb)
[【Rails】Devise で Sign up 時に confirmableモジュールを使わずに Welcome メールを送信する](https://remonote.jp/rails-devise-welcome-mail)
[[Rails] Devise｜メールアドレス認証を実装する](https://notti-blog.com/entry/rails-devise-confirmable/)
[早く知ってたら良かったrailsの技]((https://qiita.com/k-shogo/items/5bbc23e1d0dd0ad3a8a2#super-do-%E3%81%A7%E6%93%8D%E4%BD%9C%E3%82%92%E5%B7%AE%E3%81%97%E8%BE%BC%E3%82%80))
[Rails Devise でパスワードリセットなどのメールテンプレート（Mailer ビュー）をカスタマイズ](https://easyramble.com/customize-mail-template-of-devise.html)