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

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

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

#2. 第11章の概要
この章では、新規登録したユーザーに対してアカウントを有効化するステップを設けます。
ユーザーのメールアドレスにアカウントの有効化を行うためのリンクを示し、それがクリックされたら
アカウントが有効化され、ユーザーがアプリケーションを使用できるようになります。
これにより、メールアドレスの持ち主が登録したかどうかを確認できます。

1. アカウント有効化機能の実装準備
    1. 実装までの流れと処理の流れ
    1. コールバックで処理を実行する
1. メイラーを使用する
    1. メイラーを追加する
    1. 新規登録時にアカウント有効化のメールを送る
1. アカウントを有効化する
    1. authenticated?メソッドを改良する
    1. editアクションで有効化を行う
    1. 有効化していないユーザーがログインできないようにする

#3. 学習内容
###1. アカウント有効化機能の実装準備



####1-1.　処理の流れと実装までの流れ
まず、アカウント有効化機能の処理の流れを確認します。

①ユーザーの初期ステータスを、「有効化されていない」にする
②ユーザー登録が行われた時に、有効化トークンと有効化ダイジェストを生成する
③有効化ダイジェストはデータベースに保存、有効化トークンはメールアドレスと一緒に
 有効化用メールのリンクに含める
④ユーザーがメールのリンクをクリックしたら、メールアドレスからユーザーを検索し、
データベース内の有効化ダイジェストとリンク内の有効化トークンを比較して認証する
⑤ユーザーが認証できたら、ユーザーのステータスを「有効化済み」にする

以上の流れで処理が行われていきます。
このトークンとダイジェストを使用した仕組みは、以前に実装したパスワードの認証の仕組みと似ています。

次に実装までの流れを確認します。

①Userモデルに有効化ダイジェスト、有効化の状態、有効化が行われた日時を格納する属性を追加する
②新規登録時に有効化トークンと有効化ダイジェストを生成する
③有効化トークンとメールアドレスが含まれた有効化用リンクが書かれたメールを送信する
④リンクがクリックされたらステータスを変更し、アプリケーションが使用できるようにする

以上の流れでアカウント有効化機能の実装を行います。
これらの処理はアカウントの有効化という作業を「リソース」としてモデル化して実装します。

モデル化することにより、コントローラとアクションが使用できるようになります。
実装する機能ではデータベースの有効化ステータス属性を変更する処理がメインなので、
本来であればcreateアクションでその処理を実行します。
ですが、今回の場合はメールのリンクをクリックしたときに処理を実行します。
リンクをクリックしたときに発行されるリクエストは、POSTリクエストではなくGETリクエストになってしまうので、
この機能はeditアクションで実行することになります。

まず、アカウント有効化のリソースを作るために以下のコマンドでAccountActivationsコントローラを生成します。
`rails generate controller AccountActivations`
そして、メール内に書くリンクからeditアクションを実行するので、
editアクションへの名前付きルートを以下のコードをroutes.rbに追加します。
`resources :account_activations, only: [:edit]`

これにより、有効化の作業をリソース化できました。
editアクションの定義は後で行います。

次に、Userモデルへの属性の追加を行います。
ここでは、
有効化ダイジェストを格納する`activation_digest`
有効化ステータスを格納する`activated`
有効化された日時を格納する`activated_at`
上記の3つの属性を以下のコマンドで追加します。
`rails generate migration add_activation_to_users \
 activation_digest:string activated:boolean activated_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  |

ここまでで、アカウント有効化機能の実装に必要な変更ができました。

####1-2. コールバックで処理を実行する
ユーザーが新規登録を完了するにはアカウントの有効化が必要になるので、
オブジェクトが作成される前に、有効化トークンと有効化ダイジェストを生成して、
それを作成されたオブジェクトに渡さなければいけません。

このような、○○をする前に特定の処理を行うという際に使用するのがコールバックです。
以前、ユーザーをデータベースに保存する前に、メールアドレスを小文字に変換するという処理を実装しました。
そこでもbefore_saveコールバックというメソッドが使用されています。
ここでは、オブジェクトが作成される前に処理を実行したいので、
その時に使用するコールバックは**before_createコールバック**となります。

今回はcreate_activation_digestというメソッドで、
有効化トークンと有効化ダイジェストを生成することになるので、コードは以下のようになります。

```ruby:app/models/user.rb
# アクセスしようとした URL を覚えておく
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save :downcase_email
  before_create :create_activation_digest
  validates :name, presence: true, length: { maximum: 50 }

private

  # 有効化トークンとダイジェストを作成および代入する
  def create_activation_digest
    self.activation_token = User.new_token
   self.activation_digest = User.digest(activation_token)
  end
end
```



###2. メイラーを使用する
####2-1. メイラーを追加する
メイラーとは、アプリケーションからメールを送信するために使用する機能で、
動作はコントローラと似ています。

メイラーはコントローラ内で、送信先を設定したり、インスタンス変数を定義して、
メールの内容を定義するメイラービューで使用することができます。
メイラービューは1つのメソッドに対して2つ生成されます。
1つはテキストメール用のビューで、もう1つはHTMLメール用のビューです。


メイラーの生成には以下のコマンドを実行します。
`rails generate mailer UserMailer account_activation password_reset`
このコマンドにより、アカウントの有効化用のメールを送信する**account_activationメソッド**と、
12章で使用する、パスワードの再設定用のメールを送信する**password_reset**の2つのメソッドを持った、
**UserMailer**が生成されました。

以下にaccount_activationメソッドの内容と、それに対応するメイラービュー(テキストメール用)を示します。

```ruby:app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end
end
```

```app/views/user_mailer/account_activation.text.erb
Hi <%= @user.name %>,

Welcome to the Sample App! Click on the link below to activate your account:

<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
```

1つ目のコードが、メールを送信するaccount_activationメソッドです。
ユーザーを引数にとり、そのメールアドレスを送信先に設定、`subject: "Account activation"`は
メールの件名を設定しています。

メイラービューの重要な部分は、
`<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>`
この埋め込みRubyで書かれた部分です。
まず、`edit_account_activation_url`はaccount_activationコントローラのeditアクションへの名前付きルートです。
そして、第一引数に有効化トークン、第二引数にメールアドレスを渡しています。

なぜ、このように書くかというと、このリンクに有効化トークンとメールアドレスを含めるためです。
例えばユーザーの編集ページのリンクのコードは
`edit_user_url(user)`というように書きます。
このコードから生成されるリンクは`https://www.example.com/users/<userのid>/edit`です。
usersコントローラを表す部分とeditアクションを表す部分の間に、引数に渡したユーザーのidが入り、
paramsハッシュでparams[:id]で参照できます。

これと同じ様に今回のコードでは引数に有効化トークンに加えてemail: @user.emailというハッシュがあるので、
`https://www.example.com/account_activations/<有効化トークン>/edit?email=<メールアドレス>`
このようにコントローラとアクションの間に有効化トークンが入り、URLの末尾にクエリパラメータとしてメールアドレスが入ります。

editアクションでは生成されたリンクの内の有効化トークンとメールアドレスを、
params[:id], params[:email]で参照できます。




####2-2. 新規登録時にアカウント有効化のメールを送る
これでメールを送信する準備が整いました。
ユーザー登録を行うcreateアクションに、メールを送信する処理を加える変更を行います。
下に変更前のcreateアクションと変更後のcreateアクションを載せます。

```ruby:変更前のcreateアクション
def create
  @user = User.new(user_params)
  if @user.save
    log_in @user
    flash[:success] = "Welcome to the Sample App!"
    redirect_to @user
  else
    render 'new'
  end
end
```

```ruby:変更後のcreateアクション
def create
  @user = User.new(user_params)
  if @user.save
    UserMailer.account_activation(@user).deliver_now # @userにメールを送信
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url
  else
    render 'new'
  end
end
```
変更内容は、ログイン処理を行わずにメールを送っている点とそれに伴うメッセージの変更。
そして、リダイレクト先の変更の3つです。

なぜログイン処理を行わないかというと、ユーザー登録を完了してもアカウントの有効化を行うまで
ログインはできないようにしたいからです。
そして、元のリダイレクト先として設定されていたプロフィールページはログインしないと見れないページなので、
ここにリダイレクトさせる意味がなくなりその代わりとしてルートURLに変更しました。



###3. アカウントを有効化する
####3-1. authenticated?メソッドを改良する
ログイン状態の記憶の際に記憶トークンと記憶ダイジェストを比較するauthenticated?メソッドを、
有効化トークンと有効化ダイジェストを比較する処理にも流用します。

現段階で定義されているauthenticated?メソッドは以下のコードです。

```ruby:authenticated?メソッド
# トークンがダイジェストと一致したら true を返す
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```
このコードでは、remember_digestとremember_tokenを比較しています。
今回のアカウントの有効化ではactivation_digestとactivation_tokenを比較したいのですが、
authenticated?メソッド内のrememberの部分を変数として扱えると有効化の処理でもこのメソッドが使用できるようになります。

ここで使用するのがsendメソッドです。
sendメソッドは引数に、シンボルや文字列でメソッド名を渡すとメソッドが実行されます。

例：sendメソッドで配列の長さを取得する

```
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3
```

上記の例ではsendメソッドでメソッドを実行していますが、sendメソッドでは`user.send("attribute_digest")`
のように書くと属性の値を呼び出すこともできます。
そして、この引数に渡しているのは文字列なので、変数を含められます。
`user.send("#{attribute}_digest")`のように変数を含めて実行できるように変更を加えたauthenticated?メソッドが以下です。

```ruby:改良後のauthenticated?メソッド
# トークンがダイジェストと一致したら true を返す
def authenticated?(attribute, token)
  digest = send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end
```
元々1つだった引数が2つになり、第一引数に属性名を渡すことで、authenticated?メソッドが
有効化の処理にも流用できるようになりました。



####3-2. editアクションで有効化を行う
ここからは、先ほど改良したauthenticated?メソッドを使用して有効化を行えるようにします。
editアクションで、paramsハッシュからメールアドレスを受け取りユーザーを検索して有効化します。

以下のコードが有効化を行うeditアクションです。

```ruby:app/controllers/account_activations_controller.rb
def edit
  user = User.find_by(email: params[:email])
  if user && !user.activated? && user.authenticated?(:activation, params[:id])
    user.update_attribute(:activated, true)
    user.update_attribute(:activated_at, Time.zone.now)
    log_in user
    flash[:success] = "Account activated!"
    redirect_to user
  else
    flash[:danger] = "Invalid activation link"
    redirect_to root_url
  end
end
```
このアクションは、メール内のリンクがクリックされた時に実行されます。

まず、リンク内のメールアドレスからユーザーを検索しています。
その次の条件分岐では、リンク内の有効化トークンと、データベース内の有効化ダイジェストが一致しているかつ、
まだ有効化されていないユーザーに対して有効化処理を行います。
なぜ、まだ有効化されていないという条件を組み込んでいるかというと、
有効化処理の後にログイン処理を行っているからです。

もし、有効化されているユーザーでも有効化処理を行えるようにすると、
送信されたメールやメール内のリンクが盗み出された時に、第三者にログインされてしまいます。
それを防ぐためにここでは2つの条件式を利用しています。

条件式を満たしたユーザーは、update_attributeメソッドで、有効化ステータスがtrueになり、
この処理を行った日時が記録されます。


####3-3. 有効化していないユーザーがログインできないようにする
ここまでで、新規登録したユーザーに対して、メールを送りってリンクがクリックされたら
有効化処理を行うという一連の流れが実装できました。

最後に既存のログイン機能に変更を加えます。
新しく追加された機能をログイン処理に対しても適用するために、
アカウントが有効化されていない場合はログインを許可しないという処理を加えていきます。

以下に変更前のログイン処理と変更後のログイン処理を載せます。

```ruby:変更前のログイン処理
def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    log_in user
    params[:session][:remember_me] == '1' ? remember(user) : forget(user)
    redirect_back_or user
  else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
  end
end
```

```ruby:変更前のログイン処理
def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    if user.activated?
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      message = "Account not activated. "
      message += "Check your email for the activation link."
      flash[:warning] = message
      redirect_to root_url
    end
  else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
  end
end
```

変更部分としては、ユーザーのパスワードの認証が終わった後に`if user.activated?`という条件式を加えている部分です。
ここで現在の有効化ステータスを判定して、有効化されていた場合は変更前のコードと同じくログイン処理を実行します。

一方、有効化が行われていなかった場合はルートURLにリダイレクトさせてフラッシュメッセージで、
「Account not activated. Check your email for the activation link.」
と表示します。

このように有効化されていないユーザーのログインを拒否する処理は、
user.activated?メソッドで有効化ステータスを判定することで実装できます。




#4. 終わりに
この章ではアカウントの有効化の機能を追加しました。
今までいろんなアプリケーションでアカウント有効化を実際に利用していましたが、
なぜこの過程が必要なのかあまり分からないままでいました。
ですが、この章でこの過程の必要性とその機能がどのように実装されているのかが理解できました。
もうすでに手を付けている12章では、ここで実装したメイラーをまた使用してパスワードの再設定機能を実装していってます。