第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!に変更する。