Gem sorceryを使い倒す③ パスワードリセット機能

## パスワードリセット実装までの流れ

　
パスワードリセットの工程は長いので、どんな工程があるのかはじめに全体像を把握しておきましょう。

・sorceryのReset passwordモジュールをインストールする
・Mailerを作成しパスワードリセット用のメールを送信できるようにする
・コントローラとビューの作成
・letter_opener_webを導入しローカル環境でメールが送信されているか確認する
・configを導入し、開発環境と本番環境の設定を分ける

## sorceryのサブモジュールをインストール

https://github.com/Sorcery/sorcery/wiki/Reset-password

　
**【モジュールのインストール】**

まずはパスワードリセット用のモジュールをインストールします
```
rails g sorcery:install reset_password --only-submodules
```
作成されたファイルがこちら。
`config/initializers/sorcery.rb`
`app/models/user.rb`
`db/migrate/20231105121233_sorcery_reset_password.rb`

```ruby:config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password]
```
```ruby:migrate/20231105121233_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
```
<table>
  <caption>カラムの役割</caption>
  <thead>
    <tr>
      <th>カラム</th> <th>説明</th>
    </tr>
  </thead>
  <tr>
    <td> reset_password_email_sent_at </td> <td>はパスワードリセット用のメールを送信した時刻を保持するためのカラム</td>
  </tr>
  <tr>
    <td> reset_password_token_expires_at </td> <td>トークンの有効期限が切れる時刻を記録するカラム</td>
  </tr>
  <tr>
    <td> reset_password_token </td> <td>トークン文字列を保持するカラム</td>
  </tr>
  <tr>
    <td> access_count_to_reset_password_page </td> <td>パスワードリセット画面にアクセスした回数を記録するために用意されたカラムです。N回以上はパスワードリセット画面に移動できないようにするなどの機能を実装する時に用いられるカラム</td>
  </tr>
</table>

作成されたファイルを確認したらコンソールでdb:migrateします。

```
rails db:migrate
```
 
 
## Mailerの作成

　
Mailerとは、パスワードをリセットする時に自分宛にURL付きのメールが送られて、そのURL先に飛んで新しいパスワードを設定する機能です。

公式Wikiを参考に進めていきます。
　　
　　
**【パスワードリセット用のMailerを作成】**
```
rails g mailer UserMailer reset_password_email
```
作成されたファイル
`app/mailers/user_mailer.rb`
`app/views/user_mailer`
`app/views/user_mailer/reset_password_email.text.erb`
`app/views/user_mailer/reset_password_email.html.erb`
`test/mailers/user_mailer_test.rb`
`test/mailers/previews/user_mailer_preview.rb`

　
　
**【メールを送信するためのメソッドを作成】**
```ruby: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
```

`default from: 'from@example.com'`メールの送信元のアドレスを指定できます。
`mail(to: user.email,subject: 'パスワードリセット')`メールの宛先、件名を指定します。
`@user`と`@url`はmailのviewで使用します。

　
　
次に、reset_password サブモジュールを追加し、使用するメーラーを定義します。


```ruby:config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password, blabla, blablu, ...]

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.reset_password_mailer = UserMailer
  end
end
```
`user.reset_password_mailer = `
この記述が405行目にあります（現在）コメントアウトを外し、UserMailerを追加しましょう。  


  　
 **【メールの設定】**

```ruby:app/views/user_mailer/reset_password_email.html.erb
<p><%= @user.name %>様</p>

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

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

<p><a href="<%= @url %>"><%= @url %></a></p>
```
```ruby:app/views/user_mailer/reset_password_email.text.erb
<%= @user.name %>様

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

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

<%= @url %>
```
[Gem sorceryを使い倒す② ログイン機能応用編](https://qiita.com/joinus_ibuki/items/d09435418704c666ad30)で、plofileテーブルにnameを作成した場合、`<%= @user.profile.name %>様`でnameが獲得できます。



## controller作成
　
　
コントローラーを作成していきましょう。
```
rails g controller PasswordResets create edit update
```

```ruby: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
    flash[:success]= 'パスワードリセットのメールを送信しました'
  end

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

  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(params[:id])

    if @user.blank?
      not_authenticated
      return
    end

    @user.password_confirmation = params[:user][:password_confirmation]
    if @user.change_password(params[:user][:password])
      redirect_to login_path
      flash[:success]= 'パスワードがリセットされました'
    else
      render action: 'edit'
    end
  end
end
```
公式Wikiに、参考コードが記載されています。コメントアウトで処理の内容も詳しく説明されているのでこちらは省略しています。

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

 　
Userモデルにバリデーションを追記


```ruby:models/user.rb
validates :reset_password_token, presence: true, uniqueness: true, allow_nil: true
```


`uniqueness: true`
tokenは一意なものでなければならないのでユニーク成約を付けます。これがないと別の人のパスワードを変更できてしまう可能性があります。

`allow_nil: true`
対象の値がnilの場合にバリデーションをスキップするもの。
トークンの初期値はnilのため、このままではユニーク制約に引っかかりユーザーを複数登録できなくなる問題が発生してしまいます。この問題を回避するために付与します。

  

## viewファイル作成
 　
　
ログインページに下記のリンクを追加します。
```ruby:app/views/user_sessions/new.html.erb
<%= link_to 'passwordをお忘れの方はコチラ', new_password_reset_path %>
```
　
パスワード申請フォームと再設定フォームを作成します。
```ruby:password_resets/new.html.erb
<h3>パスワードリセット申請</h3>
<%= form_with url: password_resets_path do |f| %>
    <%= f.label :email %>
    <%= f.email_field :email %>
    <%= f.submit '申請' %>
<% end %>
```
```ruby:password_resets/edit.html.erb
<h3>パスワード再設定</h3>
<%= form_with model: @user, url: password_reset_path(@token) do |f| %>
    <%= render 'shared/error_message', object: f.object %>
	<%= f.label :email %>
    <%= @user.email %>

    <%= f.label :password %>
    <%= f.password_field :password %>

    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>

    <%= f.submit 'リセット' %>
<% end %>
```


## letter_opener_webの導入

　　
letter_opener_webを使えば、開発環境では実際にメールを送信することなくメールが送信されているか確認することができます。
　　

**【letter_opener_webのインストール】**

https://github.com/fgrehm/letter_opener_web

　　
　　letter_opener_webは開発環境でしか使用しない為、development配下に記述します。
```ruby:Gemfile
group :development do
  gem 'letter_opener_web', '~> 2.0'
end
```
ターミナルでbundle installします。Dockerを使用している場合はコンテナ内で実行します。
```
bundle install
```

**【ルーティングの設定】**
```ruby:routes.rb
Rails.application.routes.draw do
  mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?

end
```

```ruby:config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener_web # 送信方法を指定
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)

　　
こちらのアドレスでletter_opener_webを開けます。
http://localhost:3000/letter_opener 

  
## gem configを導入する

https://github.com/rubyconfig/config

　　
**【gemのインストール】**
configは定数を管理するgemです。configを導入することで、ローカル環境でのメールをletter_opener_web、本番環境のメールを指定したプロトコルでなど、簡単に分けることが可能です。
　　

```ruby:Gemfile
gem 'config'
```
Gemfileに記述したら`bundle install`します。

　　
**【ファイルを生成】**
```
rails g config:install
```
生成されるファイル

`config/initializers/config.rb`
`config/settings.yml`
`config/settings.local.yml`
`config/settings`
`config/settings/development.yml`
`config/settings/production.yml`
`config/settings/test.yml`
`.gitignore`



<table>
  <caption>生成された設定ファイルの役割</caption>
  <thead>
    <tr>
      <th>ファイル名</th> <th>役割</th>
    </tr>
  </thead>
  <tr>
    <td> config/initializers/config.rb </td> <td>configの設定ファイル</td>
  </tr>
  <tr>
    <td> config/settings.yml </td> <td>すべての環境で利用する定数を定義</td>
  </tr>
  <tr>
    <td> config/settings.local.yml </td> <td>ローカル環境のみで利用する定数を定義</td>
  </tr>
  <tr>
    <td> config/settings/development.yml </td> <td>開発環境のみで利用する定数を定義</td>
  </tr>
  <tr>
    <td> config/settings/production.yml </td> <td>本番環境のみで利用する定数を定義</td>
  </tr>
  <tr>
    <td> config/settings/test.yml </td> <td>テスト環境のみで利用する定数を定義</td>
  </tr>
</table>

　　
.gitignoreには以下の内容が自動で追記されます。
```ruby:.gitignore
config/settings.local.yml
config/settings/*.local.yml
config/environments/*.local.yml
```
　　
 **【開発環境での定数設定】**
```ruby:config/environments/development.rb
  config.action_mailer.delivery_method = :letter_opener_web 
  config.action_mailer.default_url_options = Settings.default_url_options.to_h
```
42行目辺りに`action_mailer`のコードがあるので、その下辺りに記述すると分かりやすいです。

`  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`を読み込んでいます。
　　
```ruby:config/settings/development.yml
default_url_options:
  host: 'localhost:3000'
```
開発環境でのhost設定です。


  　　
**【本番環境での定数設定】**
```ruby:config/environments/production.rb
config.action_mailer.default_url_options = Settings.default_url_options.to_h
```
```ruby:config/settings/production.yml
default_url_options:
  protcol: 'https'
  host: 'example.com'
```
本番環境は別回で詳しく書きます。
　　




## 参考資料
https://github.com/Sorcery/sorcery/wiki/Reset-password

https://github.com/fgrehm/letter_opener_web

https://github.com/rubyconfig/config

https://railsguides.jp/action_mailer_basics.html


## 関連記事

https://qiita.com/joinus_ibuki/items/6cdf5368d4b858abfb51

https://qiita.com/joinus_ibuki/items/d09435418704c666ad30