Rails初学者によるRailsチュートリアル学習記録⑭ 第12章

#目次
- [1. はじめに](#1-はじめに)
- [2. 第12章の概要](#2-第12章の概要)
- [3. 学習内容](#3-学習内容)
- [4. 終わりに](#4-終わりに)

#1. はじめに
- この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録を<br>つけるための記事です。
- 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに<br>間違いがある可能性があります。(その時はコメントで指摘してくださると助かります！)
- Railsチュートリアル内ではRailsの内容以外にも、gitでのバージョン管理やHerokuを使ったデプロイも<br>学習しますが、gitに関しては既に私が学習済みのため学習記録には記述しません。
- 演習の記録も省略します。

#2. 第12章の概要
この章では、ユーザーがパスワードを忘れてしまった時の、パスワードの再設定機能を実装していきます。
パスワードの再設定機能に使用する処理は、11章のアカウントの有効化で使用した処理と似ている個所が多いです。

この機能を使用する流れとしては、ログインフォームの「forgot password」というリンクをクリックしたら、
メールアドレスを入力するフォームに遷移し、メールアドレスを送信するとパスワードの再設定用のメールが届きます。
そのメール内にあるリンクをクリックすると新しいパスワードを入力するフォームに遷移し、
新しいパスワードを送信することでパスワードを更新できます。

1. パスワードの再設定機能実装の準備
    1. PasswordResetsリソースの追加
    1. Userモデルの変更
1. パスワードを再設定するためのアクション
    1. createアクションでメールを送信する
    1. editアクションでパスワードを再設定する
    1. updateアクションでパスワードを更新する

#3. 学習内容
###1. パスワードを再設定機能の実装準備



####1-1.　PasswordResetsリソースの追加
アカウントの有効化の時と同じように、パスワードを再設定するという処理を、
リソースとして扱えるようにするために、PasswordResetsリソースの作成からはじめます。

アカウントの有効化の機能では、メール内のリンクをクリックしたタイミングで、
データベース内の有効化ステータスを変更したかったので、editアクションのみを使用しましたが、
今回はフォームを生成する必要があるのでnewアクションも使用します。

以下のコマンドを利用してPasswordResetsコントローラを生成します。
` rails generate controller PasswordResets new edit`

生成されたアクションへのルーティングを追加します。
newアクションとeditアクションで生成されたビューから値を受け取り、
メールの送信やパスワードの再設定を行うcreateアクションとupdateアクションのルーティングも用意する必要があります。

そのために、以下のコードをroutes/rbの最後の行に追加します。
'resources :password_resets, only: [:new, :create, :edit, :update]'
これでPasswordResetsリソースのルーティングの追加ができました。

ついでにこのタイミングで、forgot passwordのリンクをログインフォームに追加して、
パスワード再設定画面へ遷移できるようにします。

```html:パスワード再設定画面へのリンクを追加したログインフォーム
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= link_to "(forgot password)", new_password_reset_path %>

      <%= f.password_field :password, class: 'form-control' %>
      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

```
この時点で、ログインフォームは以下の画像のようになります。(画像はRailsチュートリアル内から引用)
![スクリーンショット 2021-06-06 110034.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285050/5cdf3f7d-6fb6-ed5e-c816-38a7c230f1d4.png)


####1-2. Userモデルの変更
アカウントの有効化の機能の実装時に、
・activation_digest
・activated
・activated_at
の3つの属性をUserモデルに追加した時のように、ここでは以下の2つの属性を追加します。
・reset_digest
・reset_sent_at
前回追加した有効化した日時を格納するactivated_atは有効化機能で使用しませんでしたが、
今回追加する、パスワード再設定用のメールを送った日時を格納するreset_sent_atは、
再設定用のメールに有効期限を持たせるために使用します。

以下のコマンドで属性の追加を行います。
` rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime`
このコマンドを実行することでUserモデルは以下の表のようになります。

| 属性名 | 型名 |
|:-|:-|
| id  | integer  |
|name|string   |
|email   |string   |
|created_at|datetime   |
|updated_at   |datetime   |
|password_digest   |string   |
|remember_digest   |string   |
|admin   |boolean   |
|activation_digest   |string   |
|activated   |boolean   |
|activated_at   | datetime  |
|reset_digest   | string  |
|reset_sent_at   | datetime  |

PasswordResetsコントローラを生成したときにforgot passwordリンクを追加した時のように、
ここではパスワード再設定用のフォームを作成しておきます。
このフォームはメールアドレスの入力フィールドのみのフォームで、
ここにメールアドレスを入力して送信すると、パスワード再設定用のメールが届きます。

```html:パスワード再設定用のフォーム
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: password_resets_path, scope: :password_reset, local: true) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "Submit", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
```
このフォームは以下の画像のようになります。(画像はRailsチュートリアル内から引用)
![スクリーンショット 2021-06-06 111906.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285050/6c8758ae-4b64-c4e9-951a-fc280532ac7e.png)

ここまでで、パスワード再設定機能の実装をする準備ができました。

###2.パスワードを再設定するためのアクション
####2-1. createアクションでメールを送信する
createアクションでは、先ほど実装したフォームに入力されたメールアドレスからユーザーを探し、
生成したパスワード再設定トークンをハッシュ化した再設定ダイジェストと、その時のタイムスタンプで
データベースの属性を更新します。
それと同時にトークンとメールアドレスが入ったリンクが含まれたメールを送信します。

以下のコードがcreateアクションでのコードです。

```ruby:app/controllers/password_resets_controller.rb
def create
  @user = User.find_by(email: params[:password_reset][:email].downcase)
  if @user
    @user.create_reset_digest         #再設定トークンを生成する
    @user.send_password_reset_email   #再設定用メールを送信する
    flash[:info] = "Email sent with password reset instructions"
    redirect_to root_url
  else
    flash.now[:danger] = "Email address not found"
    render 'new'
  end
end
```
コメントを入れた2行でそれぞれトークンの生成とメールの送信を行っています。
これらのメソッドはUserモデルに追加します。

```ruby:app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  before_save :downcase_email
  before_create :create_activation_digest

  # パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token                          #トークンの生成
    update_attribute(:reset_digest, User.digest(reset_token))  #トークンをハッシュ化してreset_digestに格納
    update_attribute(:reset_sent_at, Time.zone.now)            #reset_sent_atにタイムスタンプを格納
  end

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now                #メールを送信する
  end
```

これでパスワードの再設定用のメールを送信する機能が実装できました。
ここからはメイラーの設定を行っていきます。
ただ、設定内容はアカウントの有効化の時と同じで、違う部分としてはメールの中に入れる
リンクとメールの文章くらいです。

```ruby:app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def account_activation(user)  #有効化用のメールのメソッド
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset(user)      #パスワード再設定用のメールのメソッド
    @user = user
    mail to: user.email, subject: "Password reset"  #送信先をuserのメールアドレスに、件名をPassword resetに設定
  end
end
```

```html:app/views/user_mailer/password_reset.html.erb
<h1>Password reset</h1>

<p>To reset your password click the link below:</p>

<%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email) %>

<p>This link will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>
```
これにより、
`https://example.com/password_resets/<再設定トークン>/edit?<メールアドレス>`
というリンクが含まれたメールが送信できます。

####2-2. editアクションでパスワードを再設定する
これでメールを送信する準備が整いました。
ここからはリンクをクリックされた後のeditアクションの動作を定義していきます。

まず、メール内のリンクをクリックしたときに遷移するフォームが必要です。
このフォームではパスワードの入力フィールドを2つ置き、1つは確認用として使用します。

この後、そこに入力されたパスワードでデータベースの属性を更新したいのですが、
ここで1つ、このフォーム特有の処理を加える必要があります。

それはリンクから受け取ったメールアドレスを、隠しフィールドとしてページ内に保存しておく処理です。
なぜこの処理が必要なのかというと、updateアクションでメールアドレスを基にユーザーを検索し、
そのユーザーのパスワードを更新したいのですが、
editアクションでリンクからメールアドレスを取得しても、何もしないとフォーム送信時にその情報が消えてしまうからです。

よって、隠しフィールドとしてメールアドレスを保存することで、、フォームの送信時に新しいパスワードと一緒に
メールアドレスが送信されるようにします。

```html:パスワード再設定のフォーム
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= hidden_field_tag :email, @user.email %> 

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
```
`<%= hidden_field_tag :email, @user.email %>`この部分でユーザーのメールアドレスを
隠しフィールドで保存しています。

ここからeditアクションにバリデーションを加えます。
パスワードを再設定しようとしているユーザーが有効化されているか、
リンク内の有効化トークンは最初にデータベースに保存した記憶ダイジェストと一致するかを検証します。

```ruby:app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  before_action :get_user, only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]


def edit
end

  private
    def get_user
      @user = User.find_by(email: params[:email])
    end

  # 正しいユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? &&
        @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
    end
  end
end
```



####2-3. updateアクションでパスワードを更新する
ここまでで、パスワード再設定のフォームへの遷移、メールの送信、新しいパスワードの送信までが実装できました。
ここからは、送信された新しいパスワードを基にデータベースの値を更新します。

ただ、新しいパスワードが送られたらどのような場合でも更新できてしまっては危険です。
具体的には次の3つの条件を設けます。
1. パスワード再設定の有効期限が切れていないか
2. 無効なパスワードは受け付けず、エラー文を表示する
3. 新しいパスワードが空文字列ではないか(ユーザー情報の編集で空入力を許可していた)
これらの条件を満たした時にパスワードの更新が行われるようにします。

2はrenderメソッドでフォームを再表示する。3はempty?メソッドとrenderメソッドで空文字だった時に
フォームが送信されないようにすることでそれぞれ実装できます。
1の有効期限の判断は、`password_reset_expired?`と`check_expiration`というメソッドを定義することで
実装していきます。

1つめの`password_reset_expired?`メソッドが有効期限内の時にfalse、有効期限を過ぎたときにtrueを返すメソッドで、
Userモデルに定義します。
2つめの`check_expiration`は上記の`password_reset_expired?`メソッドを呼び出し、
有効期限が切れていた時にエラー文を表示させて、
パスワード再設定用のメールアドレスを入力するフォームにリダイレクトします。


```ruby:app/models/user.rb
def check_expiration
  if @user.password_reset_expired?
    flash[:danger] = "Password reset has expired."
    redirect_to new_password_reset_url
  end
end
```


```ruby:app/controllers/password_resets_controller.rb
def check_expiration
  if @user.password_reset_expired?
    flash[:danger] = "Password reset has expired."
    redirect_to new_password_reset_url
  end
end
```

そして、上記のメソッドを使用して3つの条件を設けたupdateアクションが以下のコードです。

```ruby:updateアクション
class PasswordResetsController < ApplicationController
  before_action :get_user, only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update]   # 有効期限が切れてないかを最初に判断する
 
  def update
    if params[:user][:password].empty?                      # 空文字かどうかを判断する
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update(user_params)                         # パスワードを更新する
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'                                         # パスワードが無効の場合フォームを再表示する
    end
  end
end
```

以上で、この章で実装したかったパスワードの再設定機能が実装できました。

#4. 終わりに
この章は11章で扱った内容と似た部分が多かったので、
内容を進めるのも記事を書くのもあまり時間をかけずに進められました。
今まで実装したものはHerokuでデプロイしてるので、自分が開発したアプリで実際にユーザー登録できて感動です。

まだユーザー登録の機能しかないですが、残り2章でマイクロポストの投稿機能とユーザーのフォロー機能を
実装していきます。
どちらも内容がかなり濃いので時間がかかりそうですが頑張ります。