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.rbclass 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.rbmodule 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.rbrequire '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.rbrequire '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.rbclass 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.rbclass 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.rbclass 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.rbclass 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.rbclass 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.rbRails.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>

