railsチュートリアル　第十二章

##はじめに

railsチュートリアル第十二章を進めていく中で、理解しにくかったところなど復習用に要約しているものとなっています！

##パスワードの再設定

第十一章でアカウントの有効化が済み、十二章ではパスワードの再設定を実装していく。

また、実装していく流れは**十一章の有効化と似通っている部分**が多くある。

パスワード再設定の全体の流れは次の通り。
>
・ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける
・該当のメールアドレスがデータベースにある場合は、再設定用トークンとそれに対応するリセットダイジェストを生成する
・再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
・ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較する (トークンを認証する)
・認証に成功したら、パスワード変更用のフォームをユーザーに表示する

今回も有効化の時と同様に、新たなモデルは作らずに、代わりに必要なデータ (再設定用のダイジェストなど) を ```Userモデル``` に追加していく形で進めていく。

###PasswordResetsコントローラ
最初のステップとして、パスワード再設定用のコントローラを作っていく。

今回はビューも扱うので有効化と違い、```newアクション``` と ```editアクション``` も一緒に生成しておく。

```
$ rails generate controller PasswordResets new edit 
```

次にルーティングを設定する。
新しいパスワードを再設定するためのフォームと、Userモデル内のパスワードを変更するためのフォームが必要になるので、**new、create、edit、update**のルーティングを用意しておく。

```config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
  #追加
  resources :password_resets,     only: [:new, :create, :edit, :update]
end
```

上記のコードは以下の**RESTfulのルーティング**に従い、名前付きルートを使っている。


| HTTPリクエスト | URL | Action | 名前付きルート |
|:--|:--|:--|:--|
| GET | /password_resets/new | new | new_password_reset_path |
| POST | /password_resets | create | password_resets_path |
| GET | /password_resets/<token>/edit | edit | edit_password_reset_url(token) |
| PATCH | /password_resets/<token> | update	| password_reset_url(token) |


#


パスワード再設定画面へのリンクを追加しておく。

```app/views/sessions/new.html.erb
.
.
.
      <%= f.label :password %>
      #追加
      <%= link_to "(forgot password)", new_password_reset_path %>
      <%= f.password_field :password, class: 'form-control' %>
.
.
.
```

###新しいパスワードの設定
アカウント有効化の場合のように、パスワードの再設定でも、**トークン用の仮想的な属性とそれに対応するダイジェスト**を用意していく。

もしトークンをハッシュ化せずに (つまり平文で) データベースに保存してしまうとするとセキュリティ的にダメだから再設定でもダイジェストを使うようにする。

また、**再設定用のリンクに期限を設けるため、メールの送信時刻を記録する属性も追加する**必要がある。

```
$ rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetime
```
```
$ rails db:migrate
```

↑のように```reset_digest:string```と```reset_sent_at:datetime```を**usersテーブル**に追加した。

**新しいパスワード再設定の画面**はログインの画面を参考に以下のようなものにする。

```app/views/sessions/new.html.erb
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "Submit", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
```

###createアクションでパスワード再設定

フォームから送信を行なった後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要がある。

それに続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示する。

また送信が無効だった場合の処理も書いておく。

```app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController

  def new
  end

  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

  def edit
  end
end
```

この処理を実行できるように、```Userモデル``` に**パスワード再設定用メソッド**を追加する。

```app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
#省略
# パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

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

この時点でのアプリケーションは、無効なメールアドレスを入力した場合に正常に動作し、エラーメッセージを表示する。

正しいメールアドレスを送信した場合にもアプリケーションが正常に動作するためには、パスワード再設定のメイラーメソッドを定義する必要がある。

##パスワード再設定のメール送信
PasswordResetsコントローラで、createアクションがほぼ動作するところまできたから、次にパスワード再設定に関するメールを送信する部分を進めていく。

ここでは第十一章で生成した**Userメイラー (app/mailers/user_mailer.rb)**にデフォルトの ```password_resetメソッド``` もまとめて生成されているからこちらの方で処理をかく。

###パスワード再設定のメールとテンプレート

**UserMailer.password_reset(self).deliver_now**

上のコードを実装するために**Userメイラー**に ```password_resetメソッド``` を作成し、続いて、テキストメールのテンプレートとHTMLメールのテンプレートをそれぞれ定義していく。

```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"
  end
end
```
```app/views/user_mailer/password_reset.text.erb
To reset your password click the link below:

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

This link will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
```
```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>
```

###editアクションで再設定
無事に送信メールを生成できたので、次は**PasswordResetsコントローラ**の ```editアクション``` の実装を進めていく。

送られてきたurlをクリックして表示される、パスワード再設定のフォーム次のようなものとする。

```app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) 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>
```

この場合はメールアドレスをキーとしてユーザーを検索するためには、**editアクションとupdateアクションの両方でメールアドレスが必要**になる。

ここで**隠しフィールド**としてメールアドレスをページ内に保存することで、他の情報と一緒にメールアドレスが送信されるようになる。


```editアクション``` と```updateアクション```のどちらの場合も正当な ```@user``` が存在する必要があるので、いくつかのbeforeフィルタを使って ```@user``` の検索とバリデーションを行う。

```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
```

###パスワードを更新する

**AccountActivationsコントローラ**の ```editアクション``` では、ユーザーの有効化ステータスをfalseからtrueに変更したが、今回の場合はフォームから新しいパスワードを送信する事になるからフォームからの送信に対応する ```updateアクション``` が必要になる。

```updateアクション``` では、以下の４つを考慮する必要がある。

>１パスワード再設定の有効期限が切れていないか
２無効なパスワードであれば失敗させる (失敗した理由も表示する)
３新しいパスワードが空文字列になっていないか (ユーザー情報の編集ではOKだった)
４新しいパスワードが正しければ、更新する

これらを考慮して以下のようにを定義する。

```app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  before_action :get_user,         only: [:edit, :update]
  before_action :valid_user,       only: [:edit, :update]
  #追加
  before_action :check_expiration, only: [:edit, :update]    # (1) への対応

#省略

  def update
    if params[:user][:password].empty?                  # (3) への対応
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update_attributes(user_params)          # (4) への対応
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'                                     # (2) への対応
    end
  end

  private

    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end
    .
    .
    .
    # トークンが期限切れかどうか確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
end
```


**パスワード再設定の有効期限が切れていないか**
に関しては、```check_expirationメソッド```を定義して解決する。

check_expirationメソッドを実装するには
```password_reset_expired?```
を定義する必要がある。

このメソッドではパスワード再設定の期限を設定して、**2時間以上パスワードが再設定されなかった場合は期限切れ**とする処理を行う。

それでは**Userモデル**で　```password_reset_expired?メソッド``` を定義する。

```app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  private
    .
    .
    .
end
```

```reset_sent_at < 2.hours.ago```
は、
「パスワード再設定メールの送信時刻が、現在時刻より2時間以上前 (早い) の場合」

という風に解釈できる。


これでパスワード再設定の実装も終わり、あとは前章のように**production環境**でも動くようにする。

セットアップの手順はアカウント有効化と全く同じとなっている。

##さいごに
パスワード再設定はアカウント有効かと似通う部分が多い。

次↓
https://qiita.com/jonnyjonnyj1397/items/01a5dfeee3a4e9133266