11.3 アカウントを有効化する
メールが生成できたら、今度はAccountActivationsコントローラのeditアクション
を書いていきましょう。また、アクションへのテストを書き、しっかりとテスト
できていることが確認できたら、AccountActivationsコントローラからUser
モデルにコードを移していく作業 (リファクタリング) にも取り掛かっていきます。
11.3.1 authenticated?
メソッドの抽象化
有効化トークンとメールをそれぞれparams[:id]とparams[:email]で参照できることを思い出してみましょう。
パスワードのモデルと記憶トークンで学んだことを
元に、次のようなコードでユーザーを検索して認証することにします。
user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])
これから実装するauthenticated?メソッドでは、受け取ったパラメータに応じて呼び出すメソッドを切り替える手法を使います。
この一見不思議な手法は「メタプログラミング」と呼ばれています。
メタプログラミングを一言で言うと「プログラムでプログラムを作成する」ことです。メタプログラミングはRubyが有するきわめて強力な機能であり、
Railsの一見魔法のような機能 (「黒魔術」とも呼ばれます) の多くは、
Rubyのメタプログラミングによって実現されています。
ここで重要なのは、sendメソッドの強力きわまる機能です。
このメソッドは、渡されたオブジェクトに「メッセージを送る」ことによって、呼び出すメソッドを動的に決めることが
できます。
例を見てみましょう。Railsコンソールを開き、
Rubyのオブジェクトに対してsendメソッドを実行し、配列の長さを得るとします
rails console >> a = [1, 2, 3] >> a.length => 3 >> a.send(:length) => 3 >> a.send("length") => 3
send
を通して渡したシンボル:length
や文字列"length"
は、いずれもlength
メソッドと同じ結果になりました。つまり、どちらもオブジェクトにlength
メソッドを渡しているため、等価なのです。もう1つ例をお見せします。データベースの最初のユーザーが持つactivation_digest
属性にアクセスする例です。
>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
>> user.send("activation_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtR"
シンボル:activation
と等しいattribute
変数を定義し、文字列の式展開 (interpolation) を使って引数を正しく組み立ててから、send
に渡しています。文字列'activation'
でも同じことができますが、Rubyではシンボルを使う方が一般的です。
"#{attribute}_digest"
シンボルと文字列どちらを使った場合でも、上のコードは次のように文字列に変換されます。
"activation_digest"
sendメソッドの動作原理がわかったので、この仕組みを利用してauthenticated?メソッドを書き換えてみましょう。
authenticated?
メソッド redapp/models/user.rb
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
.
.
.
rails test
テストが失敗する理由は、current_user
メソッド とnil
ダイジェストのテストの両方で、authenticated?
が古いままになっており、引数も2つではなくまだ1つのままだからです。これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにします。
current_user
内の抽象化したauthenticated?
メソッド redapp/helpers/sessions_helper.rb
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
authenticated?
メソッド greentest/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?(:remember, '')
end
end
上のような変更を加えると、テストは greenに変わります。
rails test
演習
1:コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか?
また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
created_at: nil, updated_at: nil, password_digest: "$2a$10$D80rMhemJkoM8JmOU5IGN8YwsOHhBb...", remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil
2:抽象化したauthenticated?メソッドを使って、先ほどの
各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
動作確認するだけ
11.3.2 editアクションで有効化
editアクションを書く準備ができました。
このアクションは、paramsハッシュで
渡されたメールアドレスに対応するユーザーを認証します。
ユーザーが有効であることを確認する中核は、次の部分になります。
if user && !user.activated? && user.authenticated(:activation,params[:id])
↑
既に有効になっているユーザーを誤って再度有効化しないために必要です
上の論理値に基いてユーザーを認証するには、ユーザーを認証してからactivated_atタイムスタンプを更新する必要があります。
user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now)
アカウントを有効化するeditアクション
app/controllers/account_activations_controller.rb
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
user.activated?
がtrueの場合にのみログインを許可し、そうでない場合はルートURLにリダイレクトしてwarning
で警告を表示します。
有効でないユーザーがログインすることのないようにする
app/controllers/sessions_controller.rb
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 def destroy log_out if logged_in? redirect_to root_url end end
演習
1:コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?
Hi testerer, Welcome to the Sample App! Click on the link below to activate your account: https://ap-northeast-1.amazonaws.com/account_activations/O0VRZve0xN9l27LNySAnVQ/edit?email=exxample%40gmal.com ----==_mimepart_5e8d6fcbafbc4_11b625d352085248 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style> /* Email styles need to be inline */ </style> </head> <body> <h1>Sample App</h1> <p>Hi tt,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="https://ap-northeast-1.amazonaws.com/account_activations/xliAdIUvihhp2b1S3vD2CA/edit?email=rena%40gma.com">Activate</a> </body> </html>
O0VRZve0xN9l27LNySAnVQが有効化トークン
2:先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。
また、有効化ステータスがtrueになっていることを確認してみてください。
rails c で確認するだけ
11.3.3 有効化のテストとリファクタリング
test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
end
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
assert_template 'users/new'
assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
end
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
assert_equal 1, ActionMailer::Base.deliveries.size
user = assigns(:user)
assert_not user.activated?
# 有効化していない状態でログインしてみる
log_in_as(user)
assert_not is_logged_in?
# 有効化トークンが不正な場合
get edit_account_activation_path("invalid token", email: user.email)
assert_not is_logged_in?
# トークンは正しいがメールアドレスが無効な場合
get edit_account_activation_path(user.activation_token, email: 'wrong')
assert_not is_logged_in?
# 有効化トークンが正しい場合
get edit_account_activation_path(user.activation_token, email: user.email)
assert user.reload.activated?
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
end
本当に重要な部分は次の1行です。
assert_equal 1, ActionMailer::Base.deliveries.size
上のコードは、配信されたメッセージがきっかり1つであるかどうかを確認します。
assignsメソッドを使うと対応するアクション内のインスタンス変数にアクセスできるようになります。
例えば、Usersコントローラのcreateアクションでは@user
というインスタンス変数が定義されていますが、
テストでassigns(:user)と
書くとこのインスタンス変数にアクセスできるようになる、といった具合です。
rails test
activate
メソッドを作成してユーザーの有効化属性を更新し、send_activation_email
メソッドを作成して有効化メールを送信します。app/models/user.rb
class User < ApplicationRecord
.
.
.
# アカウントを有効にする
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
private
.
.
.
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
.
.
.
end
app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.activate
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
rails test
演習
1:activate
メソッドはupdate_attribute
を2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。
テンプレートを使って、update_attribute
の呼び出しを1回のupdate_columns
呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。
user.rb class User < ApplicationRecord (中略) # アカウントを有効にする def activate update_columns(activated: true, activated_at: Time.zone.now) end (後略)
2:現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、テンプレートを使って、この動作を変更してみましょう 。
users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy def index @users = User.where(activated: true).paginate(page: params[:page]) end def show @user = User.find(params[:id]) redirect_to root_url and return unless @user.activated? end (後略)
3:ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
update_columns
メソッドは、コールバックとバリデーションを実行せずにスキップしますので、コールバックやバリデーションをかける必要がある場合は注意が必要です。
update_columns
を使用するテンプレート
app/models/user.rb
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token
before_save :downcase_email
before_create :create_activation_digest
.
.
.
# アカウントを有効にする
def activate
update_columns(activated: FILL_IN, activated_at: FILL_IN)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
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
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def index
@users = User.where(activated: FILL_IN).paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
redirect_to root_url and return unless FILL_IN
end
.
.
.
end
11.4 本番環境でのメール送信
本番環境からメール送信するために、「Mailgun」というHerokuアドオンを利用してアカウントを検証します (このアドオンを利用するためにはHerokuアカウントにクレジットカードを設定する必要がありますが、アカウント検証では料金は発生しません)。
config/environments/production.rb
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :smtp
host = '<あなたのHerokuサブドメイン名>.herokuapp.com'
config.action_mailer.default_url_options = { host: host }
ActionMailer::Base.smtp_settings = {
:port => ENV['MAILGUN_SMTP_PORT'],
:address => ENV['MAILGUN_SMTP_SERVER'],
:user_name => ENV['MAILGUN_SMTP_LOGIN'],
:password => ENV['MAILGUN_SMTP_PASSWORD'],
:domain => host,
:authentication => :plain,
}
.
.
.
end
rails test
git add -A
git commit -m "Add account activation"
git checkout master
git merge account-activation
続いてリモートリポジトリにプッシュし、Herokuにデプロイします。
rails test
git push
git push heroku
heroku run rails db:migrate
MailgunのHerokuアドオンを追加するために、次のコマンドを実行します。
heroku addons:create mailgun:starter
注: herokuコマンドのバージョンが古いとここで失敗するかもしれません。その場合は、Heroku Toolbeltを使って最新版に更新するか、次の古い文法のコマンドを試してみてください。
heroku addons:add mailgun:starter
Herokuの環境変数を表示したい場合は、次のコマンドを実行します。
heroku config:get MAILGUN_SMTP_LOGIN heroku config:get MAILGUN_SMTP_PASSWORD
#受信メールの認証を行います heroku addons:open mailgun
演習
1:実際に本番環境でユーザー登録をしてみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
動作確認する。
メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。
また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。
ターミナルからheroku logs
コマンドを実行してみましょう。
<a href="https://<your heroku app>.herokuapp.com/account_activations/uiXkg8zRNKlluX3pX7-vwg/edit?email=mochikichi%40live.jp">Activate</a>