【Rails】パスワードリセット機能を実装する

## 実装の流れ
1. sorceryのReset passwordモジュールをインストールする
1. Mailerの作成
1. コントローラの作成
1. トークンにユニーク制約を付与
1. ビューファイルの作成
1. letter_opener_webの導入
1. configを導入

公式wikiに手順が説明されている。
https://github.com/Sorcery/sorcery/wiki/Reset-password

## sorceryのサブモジュールをインストール
```
$ rails g sorcery:install reset_password --only-submodules
```
以下のファイルが生成される。
config/initializers/sorcery.rb
app/models/user.rb
db/migrate/20231105121233_sorcery_reset_password.rb

しかし私の場合、app/models/user.rbでエラーのような文が表示された。
```
gsub  config/initializers/sorcery.rb
File unchanged! The supplied flag value not found!  app/models/user.rb
create  db/migrate/20240227224617_sorcery_reset_password.rb
```
とりあえず進める。

### 生成されたファイルを確認する
```PHP:config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password]
```
```PHP:db/migrate/20240227224617_sorcery_reset_password.rb
class SorceryResetPassword < ActiveRecord::Migration[7.0]
  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
```

| カラム | 説明 |
|:-|:-|
| reset_password_email_sent_at  |  パスワードリセット用のメールを送信した時刻を保持するためのカラム |
| reset_password_token_expires_at  |  トークンの有効期限が切れる時刻を記録するカラム |
|  reset_password_token |  トークン文字列を保持するカラム |
| access_count_to_reset_password_page  |  パスワードリセット画面にアクセスした回数を記録するために用意されたカラムです。N回以上はパスワードリセット画面に移動できないようにするなどの機能を実装する時に用いられるカラム |

### マイグレーションする
```
$ rails db:migrate
```
## パスワードリセット用のMailerを作成する
```
rails g mailer UserMailer reset_password_email
```
生成されたファイルを確認
```
create  app/mailers/user_mailer.rb
invoke  erb
create    app/views/user_mailer
create    app/views/user_mailer/reset_password_email.text.erb
create    app/views/user_mailer/reset_password_email.html.erb
```
app/mailers/user_mailer.rbが作成されている。
app/views/user_mailerの下に二つのビューファイルが作成されている。
html形式のメールとtext形式のメールが用意されているということ。

### メールを送信するメソッドを作成
```php: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: t('defaults.password_reset'))
  end
end
```
@userと@urlはmailのviewファイルで使用する。

## パスワードリセットに使用するmailerを定義する
config/initializers/sorcery.rb にreset_passwordサブモジュールを追加し、パスワードリセットに使用するActionMailerとしてUserMailerを定義する記述をする。
```php:config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password]

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.reset_password_mailer = UserMailer 
  end　
end
```
既にファイル内に記述がコメントアウトしてあったので、必要分のみ追記しました。
# user.reset_password_mailer = の部分にUserMailerを付け加え
### Mailerのビューファイルの設定
```php:app/views/user_mailer/reset_password_email.text.erb
<%= @user.decorate.full_name %>様
===============================================
 
パスワード再発行のご依頼を受け付けました。

こちらのリンクからパスワードの再発行を行ってください。

<%= @url %>
```
```php:app/views/user_mailer/reset_password_email.html.erb
<p><%= user.decorate.full_name %>様</p>

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

<p>こちらのリンクからパスワードの再発行を行ってください。</p>

<p><a href="<%= @url %>"><%= @url %></a></p>
```
@url の中身は、tokenで発行された再登録用のurlのこと。

## コントローラの作成
```
rails g controller PasswordResets create edit update
```
```
create  app/controllers/password_resets_controller.rb
invoke  erb
create    app/views/password_resets
create    app/views/password_resets/create.html.erb
create    app/views/password_resets/edit.html.erb
create    app/views/password_resets/update.html.erb
invoke  decorator
create    app/decorators/password_reset_decorator.rb
```
### コントローラの中身を記述
```php:app/controllers/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: t('.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: t('.success')
    else
      flash.now[:danger] = t('.fail')
      render :edit, status: :unprocessable_entity
    end
  end
end
```

## ルーティングの設定
```php:config/routes.rb
resources :password_resets, only: [:new, :create, :edit, :update]
```

## usersテーブルのtokenにunique制約とuniqueバリデーションを付与
tokenは一意なものでなければならないのでユニーク制約を付ける。
```php:app/models/user.rb
validates :reset_password_token, uniqueness: true, allow_nil: true
```
## ビューファイルを編集
### ログインページにパスワードリセットページへのリンクを追加
```
<%= link_to t('user_sessions.new.to_register_page'), new_user_path %>
<%= link_to t('user_sessions.new.password_forget'), new_password_reset_path %> #追記
```
### パスワードリセット申請画面を作成
```php:app/views/password_resets/new.html.erb
<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 col-lg-8 mx-auto">
      <h1><%= t('.title') %></h1>
      <%= form_with url: password_resets_path do |f| %>
        <div class="mb-3">
          <%= f.label :email, class: "form-label" %>
          <%= f.email_field :email, class: 'form-control' %>
        </div>
        <%= f.submit t('.submit'), class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>
```

### パスワードリセット画面を作成
```php:app/views/password_resets/edit.html.erb
<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 col-lg-8 mx-auto">
      <h1><%= t('.title') %></h1>
      
      <%= form_with model: @user, url: password_reset_path(@token), method: :patch do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

      <div class="mb-3">
      <%= f.label :email, class: "form-label" %>
      <%= @user.email %>
      </div>

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

      <div class="mb-3">
      <%= f.label :password_confirmation, class: "form-label" %>
      <%= f.password_field :password_confirmation, class: "form-control" %>
      </div>
      <%= f.submit class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>
```
## letter_opener_webを追加する
letter_opener_weを使うと、開発環境で実際にメールを送信せずに、メールの送信を確認できる。
https://github.com/fgrehm/letter_opener_web

開発環境でしか使わないため、development配下に記載する。
```php:Gemfile
group :development do
  gem 'letter_opener_web', '~> 2.0'
end
```
bundle installする
```
bundle install
```

### ルーティングの設定をする
```php:config/routes.rb
Rails.application.routes.draw do
  mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
end
```

```php:config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener 
config.action_mailer.default_url_options = { host: 'localhost:3000' } 
```

### メールが届くかどうかは以下の画面から確認する
http://localhost:3000/letter_opener

## configを導入する
gem configを利用して、環境ごとの定数を便利に管理できるようになる。
```php:Gemfile
gem 'config'
```
```
bundle install
```
以下のコマンドを入力してファイルを生成
```
rails g config:install
```
ファイルが生成される
```
create  config/initializers/config.rb
      create  config/settings.yml
      create  config/settings.local.yml
      create  config/settings
      create  config/settings/development.yml
      create  config/settings/production.yml
      create  config/settings/test.yml
      append  .gitignore
```
生成された設定ファイルの役割
| ファイル名 | 役割 |
|:-|:-|
| config/initializers/config.rb  |  configの設定ファイル |
| config/settings.yml  |  すべての環境で利用する定数を定義 |
| config/settings.local.yml  |  ローカル環境のみで利用する定数を定義 |
| config/settings/development.yml  |  開発環境のみで利用する定数を定義 |
| config/settings/production.yml  |  本番環境のみで利用する定数を定義 |
| config/settings/test.yml  |  テスト環境のみで利用する定数を定義 |

.gitignoreには以下の内容が自動で追記される
```php:.gitignore
config/settings.local.yml
config/settings/*.local.yml
config/environments/*.local.yml
```

## 開発環境での定数を設定

```php:config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener_web 
config.action_mailer.default_url_options = Settings.default_url_options.to_h
```
```php:config/environments/test.rb
config.action_mailer.default_url_options = Settings.default_url_options.to_h
```
config.action_mailer.delivery_method = :letter_opener_web
→Railsアプリケーションでメールを送信する際に使用する配送方法（delivery method）を設定している。

config.action_mailer.default_url_options =　Settings.default_url_options.to_h
→ActionMailerがメールを生成する際に使用するデフォルトのURLオプションを設定している。
以下のdevelopment.ymlで記述された定数default_url_optionsを読み込んでいる。


### host情報をsettings/development.ymlに記載する
ホスト情報は開発環境とテスト環境の両方に設定する
```php:config/settings/development.yml
default_url_options:
  host: 'localhost:3000'
```
```php:config/settings/test.yml
default_url_options:
  host: 'localhost:3000'
```