Rails tutorialのまとめ(第11章アカウントの有効化 主に演習)

その10から続く

第11章アカウントの有効化

アカウントを有効化する段取りは、ユーザーログイン、
特にユーザーの記憶と似ています。基本的な手順は次のようになります。

1:ユーザーの初期状態は「有効化されていない」(unactivated) にしておく。

2:ユーザー登録が行われたときに、有効化トークンと、
それに対応する有効化ダイジェストを生成する。

3:有効化ダイジェストはデータベースに保存しておき、有効化トークンは
メールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく

4:ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレス
をキーにしてユーザーを探し、データベース内に保存しておいた
有効化ダイジェストと比較することでトークンを認証する。

5:ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から
「有効化済み」(activated) に変更する。

今回実装するアカウント有効化やパスワード再設定の仕組みと、
以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多いので、
多くのアイデアを使い回すことができます。
(具体的にはUser.digestやUser.new_token、改造版のuser.authenticated?メソッドなど)。
それぞれの仕組みの似ている点をまとめてみました

検索キー  string              digest         authentication
email   password         password_digest   authenticate(password)
id      remember_token   remember_digest   authenticated?(:remember, token)
email   activation_token activation_digest authenticated?(:activation, token)
email   reset_token      reset_digest      authenticated?(:reset, token)

11.1 AccountActivationsリソース

ユーザーからのGETリクエストを受けるために、(本来であればupdateのところを)editアクションに変更して使っていきます。

11.1.1 AccountActivationsコントローラ

AccountActivationsリソースを作るために、
まずはAccountActivationsコントローラを生成してみましょう。

rails generate controller AccountActivations

有効化のメールには次のURLを含めることになります。

edit_account_activation_url(activation_token, ...)

editアクションへの名前付きルートが必要になるということです。そこでまずは、名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加します。

アカウント有効化に使うリソース (editアクション) を追加する
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]
end
HTTPリクエスト Action      名前付きルート                 URL 
GET           edit    edit_account_activation_url(token) /account_activation/<token>/edit

演習

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

テストして確認する

2:名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。

メールのURLからアクセスするため。

11.1.2 AccountActivationのデータモデル

先にアカウント有効化用のデータモデルとメイラーを作っていきますが、それが終わったらここで作ったリソースをもとにeditアクションを定義していきます。

有効化のメールには一意の有効化トークンが必要です。

仮想的な属性を使ってハッシュ化した文字列をデータベースに保存するようにします。具体的には、次のように仮想属性の有効化トークンにアクセスし、

user.activation_token

このようなコードでユーザーを認証できるようになります。

user.authenticated?(:activation, token)

activated属性を追加して論理値を取るようにします。これで、自動生成の論理値メソッドと同じような感じで、ユーザーが有効であるかどうかをテストできるようになります。

if user.activated? …

次のマイグレーションをコマンドラインで実行してデータモデルを追加すると、
3つの属性が新しく追加されます。

rails generate migration add_activation_to_users \
activation_digest:string activated:boolean activated_at:datetime

上の2行目にある > は改行を示すためにシェルが自動的に挿入する文字です。手動で入力しないよう、注意してください。(あえて表示していない)

admin属性 (リスト 10.54) のときと同様に、
activated属性のデフォルトの論理値をfalseにしておきます

アカウント有効化用の属性とインデックスを追加するマイグレーション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, default: false
    add_column :users, :activated_at, :datetime
  end
end
rails db:migrate

Activationトークンのコールバック

有効化トークンや有効化ダイジェストはユーザーオブジェクトが作成される前に作成しておく必要があります。

メールアドレスをデータベースに保存する前に、
メールアドレスを全部小文字に変換する必要があったのでした。
あのときは、before_saveコールバックにdowncaseメソッドをバインドしました。

オブジェクトが作成されたときだけコールバックを呼び出したいのです。
それ以外のときには呼び出したくないのです。
そこでbefore_createコールバックが必要になります。
このコールバックは次のように定義できます。

before_create :create_activation_digest

メソッド参照と呼ばれるもので、
こうするとRailsはcreate_activation_digest
というメソッドを探し、ユーザーを作成する前に実行するようになります。

create_activation_digestメソッド自体はUserモデル内でしか使わないので、
外部に公開する必要はありません。
7.3.2のときと同じようにprivateキーワードを
指定して、このメソッドをRuby流に隠蔽します。

private

def create_activation_digest
  # 有効化トークンとダイジェストを作成および代入する
end

今回before_createコールバックを使う目的は、トークンとそれに対応する
ダイジェストを割り当てるためです。実際の割り当ては次のように行います。

self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)

before_createコールバックの方はユーザーが作成される前に呼び出されることなので、更新される属性がまだありません。
このコールバックがあることで、User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになります。
後者のactivation_digest属性は既にデータベースのカラムとの関連付けができあがっているので、
ユーザーが保存されるときに一緒に保存されます。

Userモデルに実装すると有効化トークンは本質的に仮のものでなければならない
ので、このモデルのattr_accessorにもう1つ追加しました

Userモデルにアカウント有効化のコードを追加する green

app/models/user.rb

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

Time.zone.nowはRailsの組み込みヘルパーであり、
サーバーのタイムゾーンに応じたタイムスタンプを返します。

サンプルユーザーを最初から有効にしておく
db/seeds.rb

User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             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,
              activated: true,
              activated_at: Time.zone.now)
end

fixtureのユーザーを有効にしておく。上と同じように追加
test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true
  activated: true
  activated_at: <%= Time.zone.now %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>

malory:
  name: Malory Archer
  email: boss@example.gov
  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
rails db:seed

演習

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

password_digest: "$2a$10$xMI./
UKsJ8.dxqTZp.8QaOWxZWLL6mS...",
remember_digest: nil,
admin: true,
activation_digest:
"$2a$10$ks.AY.D.5LZrqPuEaereu7OL6qFs4FefDNm..."

2:メールアドレスの小文字化にはemail.downcase!という
(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、
downcase_emailメソッドを改良してみてください。また、うまく変更できれば、
テストスイートは成功したままになっていることも確認してみてください。

self.email.downcase!に変更する。

その11.2に続く

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です