【Rails】メール送信でパスワードリセット機能の実装

##パスワードリセット機能の実装
パスワードを忘れてしまった時に、メールアドレスを入力しパスワードを再設定する、これを``rails``での導入を行う。

その際に主に利用するツールは、``sorceryのモジュールであるreset_password``、``letter_opener_webというgem``、``configというgem``、``ActionMailer``を使って実装する。

**◆reset_password**
ユーザー認証用に導入した``gem sorcery``には、単純なパスワードによる認証機能に加え様々なモジュールが用意されている。その中の１つが``reset_password``である。
　▷他のモジュールの使い方は[ここを参考](https://github.com/Sorcery/sorcery/wiki)

**◆letter_opener_web**
``letter_opener_web``は実際のアドレスにパスワードリセットメールを送ると、毎回確認が面倒なので、メールを送信する代わりに、**デフォルトのブラウザでプレビューしてくれる**ものになる。

開発環境で電子メール配信を設定する必要がなく、誤って他の人のアドレスにテスト電子メールを送信することを心配する必要が無いため非常に便利。
（似たようなgemで``letter_opener``があるが、こちらはリモートで開発していると使えないことがあるらしいので基本的には``letter_opener_web``）

**◆config**
``config``とは環境ごとに定数を便利に管理できるGem。
アプリを開発していく中で、本番環境と開発環境で値を変えたいといったケースに使われる。定数を管理する箇所が1箇所にまとまるので保守性も非常に高まるものとなっている。

**◆ActionMailer**
Railsには、デフォルトで``ActionMailer``と呼ばれるメール送信機能が備わっている。
通常のテキストメール送信に加え、HTMLメールが送信できたり、添付ファイルを付けるなど様々なオプションがある。
 　▷[Action Mailer の基礎](https://railsguides.jp/action_mailer_basics.html)

##パスワードリセットの流れ・手順
####パスワードリセットの流れ
①パスワードリセット申請時に、ユニークなトークンを発行し、DBに保存

②メールアドレスに、パスワードリセットページへのリンクを送信。発行されたトークンをURLに組み込むことにより、どのユーザーからの申請かを判別。

③届いたメールに記載されたURLから、パスワードリセットページへと移動し、新しいパスワードを作成。

####手順
1.モジュール **reset_password**の導入
2.**Mailer**の導入
3.**reset_password** ルーティング・コントローラーの作成
4.**reset_password** のview
5.メール内容の作成・設定
6.gem **letter_opener_web**の導入・設定
7.gem **config**の導入・設定


##1.モジュール reset_passwordの導入
ターミナルにて``rails g sorcery:install reset_password --only-submodules``と入力。

すると、下記のマイグレーションファイルが作成される。

```sorcery_reset_password.rb
class SorceryResetPassword < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :reset_password_token, :string, default: nil
    add_column :users, :reset_password_token_expires_at, :datetime, default: nil
    add_column :users, :reset_password_email_sent_at, :datetime, default: nil
    add_column :users, :access_count_to_reset_password_page, :integer, default: 0

    add_index :users, :reset_password_token
  end
end
```
マイグレーションファイルが確認できたら、``rails db:migrate``を実施。
ファイル内を見ると``token``が新たにカラムとして追加されているので、``userモデル``にバリデーションを行う。

```user.rb
#以下の１行をuserモデルに追加
validates :reset_password_token, uniqueness: true, allow_nil: true
```

``uniqueness: true``
**token**は唯一無二でなけれならない。そうでないと、別の人のパスワードを変更できてしまう可能性があるため。

``allow_nil: true``
**allow_nil: true**とは対象の値がnilの場合にバリデーションをスキップするもの。
このオプションが付いていない場合は、**nil値**も重複とみなされバリデーションエラーが発生する。
→ユーザーを複数登録できなくなる問題が発生する。(トークンの初期値はnilであるため)

##2.Mailerの導入
ターミナルにて``rails g mailer UserMailer reset_password_email``と入力。

``sorcery.rb``に``reset_password``サブモジュールを追加し、パスワードリセットに使用する``ActionMailer``として``UserMailer``を定義。

```config/initializers/sorcery.rb
#デフォルトでは[]内が恐らく空になっているので、:reset_passwordを記入
Rails.application.config.sorcery.submodules = [:reset_password]

#デフォルトではコメントアウトされているので、探してUserMailerと記入
user.reset_password_mailer = UserMailer
```

##3.**reset_password** ルーティング・コントローラーの作成
**・ルーティング**
**routes.rb**にて``resources :password_resets, only: %i[new create edit update]``の一文を追記。

**・コントローラー**
ターミナルにて``rails g controller PasswordResets new create edit update``と入力。

```password_resets_controller.rb
class PasswordResetsController < ApplicationController
  skip_before_action :require_login

  def new; end

  def create
    @user = User.find_by(email: params[:email])
    @user&.deliver_reset_password_instructions!
    # ↓「存在しないメールアドレスです」という旨の文言を表示すると、逆に存在するメールアドレスを特定されてしまうため
    #　　↓あえて成功時のメッセージを送信させている
    redirect_to login_path, success: "メールを送信しました。"
  end

  def edit
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)
    not_authenticated if @user.blank?
  end
  
  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)

    return not_authenticated if @user.blank?

    @user.password_confirmation = params[:user][:password_confirmation]
    if @user.change_password(params[:user][:password])
      redirect_to login_path, success: "パスワードを変更できました"
    else
      flash.now[:danger] = "パスワード変更出来ませんでした"
      render :edit
    end
  end
end
```

``deliver_reset_password_instructions!``
　→ 有効期限付きのリセットコードを生成し、ユーザーにメールを送信するという意味

``User.load_from_reset_password_token``
　→ tokenが見つかり、有効であればユーザーを返すという意味

``change_password``
 　→ tokenをクリアして、ユーザーの新しいパスワードを更新しようとする意味


##4.reset_password のview
リセットパスワードの``new,create,edit,update``の役割は以下の通り。

・``new``
ユーザーがパスワードリセットをしたい時に、申請するための画面を表示。

・``create``
パスワードリセット対象のメールアドレスを申請して、リセットの対象を作成。

・``edit``
メールに届いた、パスワードリセット用のリンクに飛んで、新しいパスワードを入力。

・``update``
パスワードリセット(edit)ページにて、『更新する』ボタンでパスワードを変更。

viewは**new（パスワードリセット申請画面）**と**edit（新パスワード作成画面）**だけ作成。

```（パスワードリセット申請画面）new.html.rb
<% content_for(:title ,'パスワードリセット申請')%>
<div class="container">
    <div class="row">
        <div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
            <h1>パスワードリセット申請</h1>
            <%= form_with url:password_resets_path,method: :post,local: true do |f|%>
                <div class="form-group">
                    <%=f.label :email, User.human_attribute_name(:email)%>
                    <%=f.text_field :email,class: "form-control"%>
                </div>
                <%=f.submit "送信",class:"btn btn-primary"%>
            <%end%>
        </div>
    </div>
</div>
```

```（新パスワード作成画面）edit.html.rb
<% content_for(:title,"パスワードリセット")%>
<div class="container">
    <div class="row">
        <div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
            <h1>パスワードリセット</h1>
            <%= form_with model:@user,url:password_reset_path(@token),local: true do |f|%>
                <%=render "shared/error_messages",object: f.object%>
                <div class="form-group">
                    <%=f.label :email%>
                    <%=@user.email%>
                </div>

                <div class="form-group">
                    <%=f.label :password%>
                    <%=f.password_field :password,class: "form-control"%>
                </div>

                <div class="form-group">
                    <%=f.label :password_confirmation%>
                    <%=f.password_field :password_confirmation,class: "form-control"%>
                </div>
                <%=f.submit "更新する",class:"btn btn-primary"%>
            <%end%>
        </div>
    </div>
</div>

```

##5.メール内容の作成・設定
**・メール内容の作成**
メール文の作成は、``app/views/user_mailer``配下にある``reset_password_email.text.erb``、と``reset_password_email.html.erb``に記載する。

**Mailer**はメール送信の呼び出しが行われた場合、
``app/mailer``配下の``UserMailer``クラス内の、``password_reset``というメソッドが呼び出され、そこで送信先やメールのタイトルを指定後、
``password_reset.text.erb``ファイルに記載された内容が送信される。

そこで``html``ファイルにも記述する理由としては、``html``ファイルで装飾されたメールの方が見やすかったりする訳である。
逆に顧客によって``htmlメール``を嫌がる（容量が大きい、ガラケーで表示できない等）方もいるので２つのファイルに記述した方が多方面性的に優れるから。

```reset_password_email.html.rb
<p><%= @user.decorate.full_name %>様</p>
<p>===============================================</p>

<p>パスワード再発行のご依頼を受け付けました。</p>

<p>こちらのリンクからパスワードの再発行を行ってください。</p>
<p><a href="<%= @url %>"><%= @url %></a></p>
```

```reset_password_email.text.rb
<%= @user.decorate.full_name %>様
===============================================

パスワード再発行のご依頼を受け付けました。

こちらのリンクからパスワードの再発行を行ってください。
<%= @url %>
```

**・設定**
``app/mailers/user_mailer.rb``にて、先ほど作成したパスワードリセットするためのメール本文を送信するためのメソッドを定義する。

```app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: "from@example.com"

  def reset_password_email(user)
    @user = User.find(user.id)
    @url = edit_password_reset_url(@user.reset_password_token)
    mail(to: user.email,subject: "パスワードリセット")
  end

end
```

``default from: 'メールの送信元'``
 → メールの送信元のアドレスを指定できる。

``mail(to: 宛先,subject: '件名')``
 → メールの宛先、件名を指定

##6.gem letter_opener_webの導入・設定
開発環境では実際のメールは送られないように設定するために、``letter_opener_web``を入れる。

**・導入**
**Gemfile**に``gem 'letter_opener_web'``を記入して、``bundle install``を実施。

**・設定**
``letter_opener_web``では、ブラウザ上でメールを確認するため、
ルーティングに``LetterOpenerWeb``にアクセスするために必要な記述を追記。

```routes.rb
#以下の1文を追記
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
```
``config/environments/development.rb``にて以下の2文を追記。

```config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
```

``config.action_mailer.delivery_method``
 → 配信方法を指定。

``config.action_mailer.perform_deliveries``
 → メールを実際に配信するかどうかを指定。デフォルトは**true**となっている。

``action_mailer``のさまざまな設定に関しては[【ここ】](https://railsguides.jp/configuring.html#action-mailer%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)に詳細が記載。

**メールが送られたか確認する場合にはここで確認 ⏩ http://localhost:3000/letter_opener**

##7.gem configの導入・設定
環境ごとに違う定数を使いたいため、この``config``を用いて``host情報``を切り分ける。

**・導入**
**Gemfile**に``gem 'config'``を記入して、``bundle install``を実施。

**・設定**
ターミナルにて``rails g config:install``を入力。
すると環境ごとのファイルが生成される。

＊全て**config**ファイル配下

| ファイル名 | 役割 |
|:-:|:-:|
|   initializers/config.rb|  configの設定ファイル |
|settings.yml   |  すべての環境で利用する定数を定義 |
|  settings.local.yml |  ローカル環境のみで利用する定数を定義 |
| settings/development.yml  | 開発環境のみで利用する定数を定義  |
| settings/production.yml  | 本番環境のみで利用する定数を定義  |
|  settings/test.yml | テスト環境のみで利用する定数を定義  |

今回は開発環境のみで行うので``config/settings/development.yml``に設定を記述する。

```config/settings/development.yml
default_url_options:
  host: 'localhost:3000'
```

##参考
[Sorcery/sorcery](https://github.com/Sorcery/sorcery/wiki/Reset-password)
[⭐️パスワードリセット機能の実装](https://sasakix10.hateblo.jp/entry/2021/05/31/181415)
[【Rails】パスワード変更（トークンがどのように使用されているのか）](https://qiita.com/ryota21/items/2f4084a8f255e2f7d058)
[gem configを理解する ~ configを使った定数管理の方法](https://qiita.com/tanutanu/items/8d3b06d0d42af114a383)
[gem letter_opener を試してみる](https://qiita.com/tanutanu/items/c6193c4c2c352ac152ec)