Railsチュートリアル 第11章 アカウントの有効化 - AccountActivationsリソース

# 前提

* アカウントの有効化機能は、セッション機能と同様、RESTfulなリソースとしてモデル化する
    * AccountActivationsコントローラーの実装は、Sessionsコントローラーの実装と似た形になる
    * ただし、以下のユースケースから、ここまで実装してきたリソースとは異なる形の実装となる
* 「有効化リンクはメールでユーザーに送信される。ユーザーが当該リンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というユースケースである
    * Webブラウザでリンクをクリックした場合と同様、このときユーザーから送られるのは`GET`リクエストである
    * `GET`リクエストが直接のトリガーになるゆえ、「`edit`アクションで直接RDBの内容が変更される」という実装になる

# AccountActivationsコントローラー

## AccountActivationsコントローラーの生成

まずは`rails generate controller`コマンドにより、AccountActivationsコントローラーを生成するところから始まります。この部分は、Sessionsコントローラーと同じ手順ですね。

```
# rails generate controller AccountActivations
Running via Spring preloader in process 13245
      create  app/controllers/account_activations_controller.rb
      invoke  erb
      create    app/views/account_activations
      invoke  test_unit
      create    test/controllers/account_activations_controller_test.rb
      invoke  helper
      create    app/helpers/account_activations_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/account_activations.coffee
      invoke    scss
      create      app/assets/stylesheets/account_activations.scss
```

## AccountActivationsリソースに関係するルーティング

AccountActivationsリソースのユースケースは、「ユーザーがリンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というものです。当然ながら、「アカウントの有効化プロセスを開始するためのリンク」が必要になってきます。さらに、「RESTfulなリソースとしてモデル化する」という前提があります。というわけで、「アカウントの有効化プロセスを開始するためのリンク」は、「AccountActivationsリソースの`edit`アクションに紐付けされたリンク」ということになります。

```
edit_account_activation_url(activation_token, ...)
```

AccountActivationsリソースのユースケースは、「ユーザーがリンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というものに限られます。（`edit`アクションではなく）リソースそのものへの`GET`や、（Usersリソースとセットでない）単独での`POST`・`DELETE`、`UPDATE`、以上のリクエストが送られてくることは想定されません。ゆえに、必要となるルーティングは`edit`のみに限られます。

```
resources :account_activations, only: [:edit]
```

結果、Railsのルーティング（`config/routes.rb`）は、以下のように変更する必要があります。

```diff: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'
    post    '/signup',  to: 'users#create'
    get     '/login',   to: 'sessions#new'
    post    '/login',   to: 'sessions#create'
    delete  '/logout',  to: 'sessions#destroy'
    resources :users
+   resources :account_activations, only: [:edit]
  end
```

# 演習 - AccountActivationsコントローラー

## 1. 現時点でテストスイートを実行すると`green`になることを確認してみましょう。

```
# rails test
Running via Spring preloader in process 13277
Started with run options --seed 33183

  43/43: [=================================] 100% Time: 00:00:15, Time: 00:00:15

Finished in 15.05852s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
```

確かにテストは成功しますね。

## 2. [表 11.2](https://railstutorial.jp/chapters/account_activation?version=5.1#table-RESTful_account_activations)の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。

> ヒント: 私達はこれからメールで名前付きルートを使います。

「AccountActivationsリソースの`edit`アクションに対応するURLは、メールに記載するURLとして使われる」というのが前提となる、というのが大きなポイントです。

* AccountActivationsリソースの`edit`アクションには、Railsアプリケーションの**外部から**アクセスできなければならない
* Railsアプリケーションの外部からリソースにアクセスする場合、完全なURLが必要となる

このあたりが理由でしょうか。

# AccountActivationのデータモデル

## RDBには、有効化トークンをハッシュ化したものを保存する

「生の有効化トークンをRDBに保存する」という運用は、万が一RDBそのものの内容が漏洩した場合、容易に攻撃に悪用される脆弱な運用です。例えば、「攻撃者が新しく登録されたユーザーの有効化トークンを盗み取り、本来のユーザーが使う前に当該トークンを使ってしまう（そして当該ユーザーとしてログインしてしまう）」という攻撃が行われる危険性が想定できます。

というわけで、パスワードや記憶トークンと同様、有効化トークンについても、「RDBに保存するのはハッシュ化した値」という運用を行うこととします。

### 実装手法

実装手法は、[節6.3](https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-adding_a_secure_password)における`password`仮想属性の実装、ならびに、[節9.1](https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-remember_me)における`remember_token`仮想属性の実装と類似しています。今回実装するのは、`activation_token`という仮想属性となります。

```ruby:
user.activation_token
```

最終的には、以下のようなコードでユーザーの有効化トークンを認証できるようになることを目指します。

```ruby:
user.authenticated?(:activation, token)
```

なお、`authenticated?`メソッドそのものの実装にも手を加えていくことになります。

## Userモデルの実装内容を変更する

Userモデルには、以下3つの属性を新たに追加していくことになります。

* `activation_digest`
    * string型
    * 有効化トークンに対するダイジェスト
* `activated`
    * boolean型
    * ユーザーが有効化されたか否か
    * デフォルトでは`false`である
* `activated_at`属性
    * datetime型
    * ユーザーが有効化された日時

上記属性追加を反映した新たなUserモデルの全体像は、以下のようになります。

![User_full.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/5535839e-a093-14b5-ca5b-c04e254af158.png)

### 新たなUserモデルに対応するマイグレーション

まずはマイグレーションそのものを生成します。

```
root@705320d4d96d:/var/www/sample_app# rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
Running via Spring preloader in process 13329
      invoke  active_record
      create    db/migrate/[timestamp]_add_activation_to_users.rb
```

`activated`属性のデフォルト値を`false`とするため、生成されたマイグレーションを以下のように書き換えていきます。

```diff:db/migrate/[timestamp]_add_activation_to_users.rb
  class AddActivationToUsers < ActiveRecord::Migration[5.1]
    def change
      add_column :users, :activation_digest, :string
-     add_column :users, :activated, :boolean
+     add_column :users, :activated, :boolean, default: false
      add_column :users, :activated_at, :datetime
    end
  end
```

最後は`rails db:migrate`です。

```
# rails db:migrate
== [timestamp] AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
   -> 0.0169s
-- add_column(:users, :activated, :boolean, {:default=>false})
   -> 0.0015s
-- add_column(:users, :activated_at, :datetime)
   -> 0.0018s
== [timestamp] AddActivationToUsers: migrated (0.0205s) ====================

```

## Active Recordのコールバックメソッドにより、ユーザーオブジェクト作成前に有効化トークン・有効化ダイジェストが生成されるようにする

### `before_create`コールバック

「オブジェクトの新規作成時にのみ呼び出される」というコールバックです。引数として、メソッド名を表すシンボルを指定（あるいは実行する処理の内容をブロックとして直接記述）します。

今回は、「有効化トークン・有効化ダイジェストを生成する処理（`create_activation_digest`メソッドとします）」を`before_create`コールバックの対象とします。

```ruby:
before_create :create_activation_digest
```

### `create_activation_digest`メソッド

有効化トークンと、有効化トークンに対応するダイジェストを生成する処理の実体を記述するメソッドです。

```ruby:
private

  def create_activation_digest
    self.activation_token = User.new_token
    self.activation_digest = User.digest(activation_token)
  end
```

#### `private`メソッドとの関係

`create_activation_digest`の前に`private`メソッドが呼び出されているのは、一つの大きなポイントです。Userモデルのbeforeフィルターで呼び出されるメソッドは、Userモデル内でしか使わないので、`User`クラス内で`private`メソッドを呼び出して以降に記述すべきとされています。そういえば、UsersコントローラーにStrong Parametersやbeforeフィルターを実装した際にも`private`メソッドを使いましたよね。

#### 有効化トークンと、有効化トークンに対応するダイジェストを生成する処理の実体

表題記載の処理の実体は、以下2つの処理となります。

```ruby:
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
```

##### `User#remember`メソッドとの類似点と相違点

類似する処理として、永続cookiesに関する記憶トークンと記憶ダイジェストを生成する`User#remember`メソッドを、[Railsチュートリアル本文の第9章で定義していました。](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-user_model_remember)

```ruby:User#remember
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end
```

`User#new_token`メソッドの使い方は、`remember`でも`create_activation_digest`でも同じです。

一方、`User#digest`メソッドの使い方は、`remember`と`create_activation_digest`で異なります。

* `remember`における`User#digest`メソッドは、`update_attribute`メソッドの引数として呼び出している
    * すでにRDB上に存在するユーザー情報を対象とするため
* `create_activation_digest`における`User#digest`メソッドは、実行結果を`self.activation_digest`に代入している
    * まだRDB上に存在しないユーザー情報を対象とするため
    * RDB上にユーザー情報が生成されるのは、`create_activation_digest`が呼び出された**後**である

### Userモデルへの実装の追加・変更の全体像

Userモデルへの実装の追加・変更の全体像は、以下のようになります。なお、既存の「メールアドレスをすべて小文字にする」という処理も、`downcase_email`というメソッドを呼び出す実装に変更しています。

```diff:app/models/user.rb
  class User < ApplicationRecord
-   attr_accessor :remember_token,
+   attr_accessor :remember_token, :activation_token
-   before_save { email.downcase! }
+   before_save :downcase_email
+   before_create :create_activation_digest
    ...略
+
+   private
+
+     # メールアドレスをすべて小文字にする
+     def downcase_email
+       self.email = email.downcase
+     end
+
+     # 有効化トークンとダイジェストを作成および代入する
+     def create_activation_digest
+       self.activation_token = User.new_token
+       self.activation_digest = User.digest(activation_token)
+     end
  end
```

#### この時点でテストは全て成功するはず

特に「RDBに格納されるデータにおいて、メールアドレスが全て小文字であるか」というテストが成功することは、再度確認が必要です。

```ruby:test/models/user_test.rb（57行目）
test "email addresses should be saved as lower-case" do
```

```
# rails test test/models/user_test.rb:57
Running via Spring preloader in process 13346
Started with run options --seed 25681

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.20554s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
```

「RDBに格納されるデータにおいて、メールアドレスが全て小文字であるか」というテストは、無事成功しました。

続いて全体のテストを実行してみましょう。

```
# rails test
Running via Spring preloader in process 13361
Started with run options --seed 7529

  43/43: [=================================] 100% Time: 00:00:06, Time: 00:00:06

Finished in 7.00017s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
```

こちらも無事成功しました。

## サンプルユーザーの生成とテスト

### サンプルユーザーを最初から有効にしておく

```diff:db/seeds.rb
  User.create(name:                 "Example User",
              email:                "example@railstutorial.org",
              password:             "foobar",
              password_confirmation: "foobar",
-             admin: true)
+             admin: true,
+             activated: true,
+             activated_at: Time.zone.now)

  99.times do |n|
    name = Faker::Name.name
    email = "example-#{n+1}@railstutorial.org"
    password = "password"
    User.create!( name:                  name,
                  email:                 email,
                  password:              password,
-                 password_confirmation: password)
+                 password_confirmation: password,
+                 activated: true,
+                 activated_at: Time.zone.now)
  end
```

### fixtureのユーザーを最初から有効にしておく

```diff:test/fixtures/users.yml
  rhakurei:
    name: Reimu Hakurei
    email: rhakurei@example.com
    password_digest: <%= User.digest('password') %>
    admin: true
+   activated: true
+   activated_at: Time.zone.now

  mkirisame:
    name: Marisa Kirisame
    email: example.example@example.org
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now

  skomeiji:
    name: Satori Komeiji
    email: example_example@example.net
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now

  rusami:
    name: Renko Usami
    email: example0@example.com
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now

  <% 30.times do |n| %>
  user_<%= n %>:
    name:  <%= "User #{n}" %>
    email: <%= "user-#{n}@example.com" %>
    password_digest: <%= User.digest('password') %>
+   activated: true
+   activated_at: Time.zone.now
  <% end %>
```

### サンプルデータを再度生成し直す

```
# rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
...略
== [timestamp] AddActivationToUsers: migrated (0.0165s) ====================

# rails db:seed
# 
```

「サンプルデータの入力におけるtypo」というのは案外やってしまいがちなので、この場面は結構緊張する場面です。今回は何事もなく完了しました。

# 演習 - AccountActivationのデータモデル

## 1. 本項での変更を加えた後、テストスイートが`green`のままになっていることを確認してみましょう。

```
# rails test
Running via Spring preloader in process 13392
Started with run options --seed 56186

  43/43: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.90673s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
```

特にfixtureの変更時におけるtypoには注意が必要です。

## 2.1. コンソールからUserクラスのインスタンスを生成し、そのオブジェクトから`create_activation_digest`メソッドを呼び出そうとすると (Privateメソッドなので) `NoMethodError`が発生することを確認してみましょう。

```
# rails console --sandbox

>> user = User.first

>> user.create_activation_digest
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x00007fdd8823ae98>)
Did you mean?  restore_activation_digest!
```

## 2.2. また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

```
>> user.activation_digest
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"
```

## 3.1. `downcase!`メソッドを使って、[リスト 11.3](https://railstutorial.jp/chapters/account_activation?version=5.1#code-user_model_activation_code)の`downcase_email`メソッドを改良してみてください。

> [リスト 6.34](https://railstutorial.jp/chapters/modeling_users?version=5.1#code-downcase_bang)で、メールアドレスの小文字化には`email.downcase!`という (代入せずに済む) メソッドがあることを知りました。

```diff:User#downcase_email
  def downcase_email
-   self.email = email.downcase
+   self.email.downcase!   
  end
```

## 3.2. また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

```
# rails test test/models/user_test.rb:57
Running via Spring preloader in process 13416
Started with run options --seed 43627

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.23544s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
```

```
# rails test
Running via Spring preloader in process 13429
Started with run options --seed 15928

  43/43: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.22996s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
```