#アカウントの有効化
現時点のアプリケーションは、新規登録したユーザーは初めからすべての機能にアクセスできるようになっている (第7章)。本章では、アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのかどうかを確認できるようにする。
アカウントを有効化する段取りは、次のようになる。
1.ユーザーの初期状態は「有効化されていない」(unactivated) にしておく。
2.ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。
4.ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、メールのリンクに含まれる有効化トークンを、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
5.ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated) に変更す
##AccountActivationsリソース
セッション機能を使って、アカウントの有効化という作業を「リソース」としてモデル化する。この作業に必要なデータ (有効化トークンや有効化ステータスなど) をUserモデルに追加する。
なお、アカウント有効化もリソースとして扱いたいが、いつもとは少し使い方が異なる点に注意する。例えば、有効化用のリンクにアクセスして有効化のステータスを変更する部分では、RESTのルールに従うとPATCHリクエストとupdateアクションになるべきである。しかし、有効化リンクはメールでユーザーに送られることを思い出そう。ユーザーがこのリンクをクリックすれば、それはブラウザで普通にクリックしたときと同じであり、その場合ブラウザから発行されるのは (updateアクションで使うPATCHリクエストではなく) GETリクエストになってしまう。このため、ユーザーからのGETリクエストを受けるために、(本来であればupdateのところを) editアクションに変更して使っていく。
###AccountActivationsコントローラ
UsersリソースやSessionsリソースのときと同様に、AccountActivationsリソースを作るために、まずはAccountActivationsコントローラを生成する。
$ rails generate controller AccountActivations
有効化のメールには次のURLを含める。
edit_account_activation_url(activation_token, ...)
これは、editアクションへの名前付きルートが必要になるということである。そこでまずは、名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加する。
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
次にアカウント有効化用のデータモデルとメイラーを作っていくが、それが終わったらここで作ったリソースをもとにeditアクションを定義していく。
###AccountActivationのデータモデル
効化のメールには一意の有効化トークンが必要なので、パスワードの実装や記憶トークンの実装と同じように仮想的な属性を使ってハッシュ化した文字列をデータベースに保存するようにする。具体的には、次のように仮想属性の有効化トークンにアクセスし、
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
activated属性のデフォルトの論理値をfalseにしておく。
class AddActivationToUsers < ActiveRecord::Migration[5.0]
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
ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になるので、有効化トークンや有効化ダイジェストはユーザーオブジェクトが作成される前に作成しておく必要がある。そこでbefore_createコールバック(オブジェクトが作成される前に呼び出されるメソッド)が必要になり、このコールバックは次のように定義できる。
before_create :create_activation_digest
create_activation_digestメソッド自体はUserモデル内でしか使わないので、外部に公開する必要はない。privateキーワードを指定して、このメソッドをRuby流に隠蔽する。
private
def create_activation_digest
# 有効化トークンとダイジェストを作成および代入する
end
クラス内でprivateキーワードより下に記述したメソッドは自動的に非公開となる。
今回before_createコールバックを使う目的は、トークンとそれに対応するダイジェストを割り当てるためで、実際の割り当ては次のように行う。
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
上で説明したことをUserモデルに実装すると下のようになる。有効化トークンは本質的に仮のものでなければならないので、このモデルのattr_accessorにもう1つ追加した。
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
先に進む前に、サンプルデータとfixtureも更新し、テスト時のサンプルとユーザーを事前に有効化しておく。
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
データベースを初期化して、サンプルデータを再度生成し直し、上の変更を反映する。
$ rails db:migrate:reset
$ rails db:seed
##アカウント有効化のメール送信
データのモデル化が終わったので、今度はアカウント有効化メールの送信に必要なコードを追加する。このメソッドではAction Mailerライブラリを使ってUserのメイラーを追加する。このメイラーはUsersコントローラのcreateアクションで有効化リンクをメール送信するために使う。
###送信メールのテンプレート
メイラーは、モデルやコントローラと同様にrails generateで生成できる。
$ rails generate mailer UserMailer account_activation password_reset
これで今回必要となるaccount_activationメソッドと、第12章で必要となるpassword_resetメソッドが生成された。
また、このコードにより、生成したメイラーごとに、ビューのテンプレートが2つずつ生成される。1つはテキストメール用のテンプレート、1つはHTMLメール用のテンプレートである。
アカウント有効化に使うテンプレートを以下に示す。
UserMailer#account_activation
<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb
<h1>UserMailer#account_activation</h1>
<p>
<%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>
生成されたメイラーの動作を簡単に追ってみよう。Applicationメイラーにはデフォルトのfromアドレス (アプリケーション全体で共通) があり、Userメイラーの各メソッドには宛先メールアドレスもある。生成されたコードにはインスタンス変数@greetingも含まれており、このインスタンス変数は、ちょうど普通のビューでコントローラのインスタンス変数を利用できるのと同じように、メイラービューで利用できる。
Applicationメイラー
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout 'mailer'
end
Userメイラー
class UserMailer < ApplicationMailer
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.account_activation.subject
#
def account_activation
@greeting = "Hi"
mail to: "to@example.org"
end
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.password_reset.subject
#
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end
最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにする (リスト 11.11)。次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、user.emailにメール送信する(リスト 11.12)。リスト 11.12では、mailにsubjectキーを引数として渡している。この値は、メールの件名にあたる。
リスト 11.11: fromアドレスのデフォルト値を更新したアプリケーションメイラー
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout 'mailer'
end
リスト 11.12: アカウント有効化リンクをメール送信する
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end
テンプレートビューは、通常のビューと同様ERBで自由にカスタマイズできる。ここでは挨拶文にユーザー名を含め、カスタムの有効化リンクを追加する。この後、Railsサーバーでユーザーをメールアドレスで検索して有効化トークンを認証できるようにしたいので、リンクにはメールアドレスとトークンを両方含めておく必要がある。これには名前つきルートに対して次のようにハッシュを追加し、リンクのURLにクエリパラメータとしてメールアドレスを組み込む。
なお、クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したものである。
edit_account_activation_url(@user.activation_token, email: @user.email)
こうすることで次のようなURLが生成される
http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
上のURLの「q5lt38hQDc_959PVoo6b7A」という部分はnew_tokenメソッドで生成されたものであり、これはちょうど/users/1/editの「1」のようなユーザーIDと同じ役割を果たす。このトークンは、特にAccountActivationsコントローラのeditアクションではparamsハッシュでparams[:id]として参照できる。(メールアドレスの「@」記号がURLでは「%40」となっている点に注目。これは「エスケープ」と呼ばれる手法で、通常URLでは扱えない文字を扱えるようにするために変換されている。)
参考
edit_user_url(user)
このコードで
http://www.example.com/users/1/edit
のURLが生成される。
以上で、@userインスタンス変数、editへの名前付きルート、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) %>
<h1>Sample App</h1>
<p>Hi <%= @user.name %>,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
email: @user.email) %>
###ユーザーのcreateアクションを更新
あとはユーザー登録を行うcreateアクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができる。
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
UserMailer.account_activation(@user).deliver_now
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
.
.
.
end
ここではリダイレクト先をプロフィールページからルートURLに変更し、かつユーザーは以前のようにログインしないようになっている。
##アカウントを有効化する
メールが生成できたら、今度はAccountActivationsコントローラのeditアクションを書いていこう。
###authenticated?メソッドの抽象化
ここで、有効化トークンとメールをそれぞれparams[:id]とparams[:email]で参照できることを思い出そう。次のようなコードでユーザーを検索して認証することにする。
user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])
上のコードで使っているauthenticated?メソッドは、アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェックする。ただし、このメソッドは記憶トークン用なので今は正常に動作しない。
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
これを一般化するため、次のようなコードを書く
def authenticated?(attribute, token) #attributeにrememberかactivateが入る
digest = self.send("#{attribute}_digest") #モデルの属性である〜_digestにアクセス
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token) #渡されたトークンがダイジェストと一致すればtrue
end
上のコードはモデル内にあるので、selfは省略できる。
ここまでできれば、次のように呼び出すことでauthenticated?の従来の振舞いを再現できる。
user.authenticated?(:remember, remember_token)
以上の説明を実際のUserモデルに適用した、抽象化したauthenticated?メソッドをリスト以下に示す。
class User < ApplicationRecord
.
.
.
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
.
.
.
end
このままだとテストに失敗する。authenticated?が古いままになっており、引数も2つではなくまだ1つのままだから。これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにする。
module SessionsHelper
.
.
.
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end
###editアクションで有効化
authenticated?が整備されたことで、やっとeditアクションを書く準備ができた。このアクションは、paramsハッシュで渡されたメールアドレスに対応するユーザーを認証する。ユーザーが有効であることを確認する中核は、次の部分になる。
if user && !user.activated? && user.authenticated?(:activation, params[:id])
!user.activated?という記述に注意する。このコードは、既に有効になっているユーザーを誤って再度有効化しないために必要である。
上の論理値に基いてユーザーを認証するには、ユーザーを認証してからactivated_atタイムスタンプを更新する必要がある。
user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)
以上を踏まえると、editアクションは以下のようになる。
class AccountActivationsController < ApplicationController
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
end
これで、有効化メールのリンクを踏むと、アカウントが有効化されるようになった。
この時点ではユーザーのログイン方法を変更していないので、ユーザーの有効化にはまだ何の意味もない。ユーザーの有効化が役に立つためには、ユーザーが有効である場合にのみログインできるようにログイン方法を変更する必要がある。
有効でないユーザーがログインすることのないようにする
class SessionsController < ApplicationController
def new
end
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
def destroy
log_out if logged_in?
redirect_to root_url
end
end
これで、ユーザー有効化機能のおおまかな部分については実装できた。