LoginSignup
0
0

More than 1 year has passed since last update.

rails_tutorial chapter11 <忘備録>

Posted at

11章では、メールを使ったアカウントの有効化を実装します。
cookiesの実装と流れは似ているので、そんなに難しくはありません。
強いて言えば、mailerとsendメソッドを初めて使ったので少し戸惑いました。

ただ、mailerはcontrollerだと思えば、MVCの考え方に乗っ取っているので、分かりやすいです。

今回の流れは、以下の通りです。

(1)有効化トークンやダイジェストを関連付けておいた状態で
(2)有効化トークンを含めたリンクをユーザーにメールで送信し
(3)ユーザーがそのリンクをクリックすると有効化できるようにする

ポイントとしては、

・ユーザーの初期状態は「有効化されていない」(unactivated)にしておく。
・ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
・有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。
※editアクションの代わりに、showアクションでもよいがshowアクションは何かを表示するために使うのが一般的なので、他の開発者が意図を汲み取りづらいのでeditがベター。

ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated)に変更する。

トークン・ダイジェスト・認証の一覧

digestは後にsendメソッドでリファクタリングします。

1 2 3 4
検索キー パスワード・トークン ダイジェスト 認証
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)

AccountActivationscontrollerの作成

$ rails generate controller AccountActivations

routingの設定

今回は、editアクションしか使用しないので、only:[: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

editのURL

トークンとなっているところは通常はidが入る。まあトークンもidと同じようなもの。

account_activation/トークン/edit

activation_digestとactivated_atカラムの追加

activation_digestはbooleanでdefaultをfalseとする。

$ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[6.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
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
before_create :create_activation_digest

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

サンプルユーザーの作成

db/seeds.rb
# メインのサンプルユーザーを1人作成する
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で生成できます。

Userメイラーの生成

$ rails generate mailer UserMailer account_activation password_reset

実行したことで、今回必要となるaccount_activationメソッドと、password_resetメソッドが生成されました。

生成したメイラーごとに、ビューのテンプレートが2つずつ生成されます。
1つはテキストメール用のテンプレート、1つはHTMLメール用のテンプレートです。

Applicationメイラー

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout 'mailer'
end

生成されたUserメイラー

app/mailers/user_mailer.rb
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

fromアドレスのデフォルト値を更新したアプリケーションメイラー

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout 'mailer'
end

edit_user_url(user)
上のメソッドは、次の形式のURLを生成します。

https://www.example.com/users/1/edit

これに対応するアカウント有効化リンクのベースURLは次のようになります。

https://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit

上のURLの「q5lt38hQDc_959PVoo6b7A」という部分はnew_tokenメソッドで生成されたものです。URLで使えるようにBase64でエンコードされています。

これはちょうど/users/1/editの「1」のようなユーザーIDと同じ役割を果たします。
このトークンは、特にAccountActivationsコントローラのeditアクションではparamsハッシュでparams[:id]として参照できます。

クエリパラメータを使って、このURLにメールアドレスもうまく組み込んでみましょう。クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したものです。

account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

このとき、メールアドレスの「@」記号がURLでは「%40」となっている点に注目してください。これは「エスケープ」と呼ばれる手法で、通常URLでは扱えない文字を扱えるようにするために変換されています。Railsでクエリパラメータを設定するには、名前付きルートに対して次のようなハッシュを追加します。

edit_account_activation_url(@user.activation_token, email: @user.email)
このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープしてくれます。コントローラでparams[:email]からメールアドレスを取り出すときには、自動的にエスケープを解除してくれます。

アカウント有効化のテキストビュー

app/views/user_mailer/account_activation.text.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) %>
リスト 11.14:アカウント有効化のHTMLビュー
app/views/user_mailer/account_activation.html.erb
<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) %>

送信メールのプレビュー

定義したテンプレートの実際の表示を簡単に確認するために、メールプレビューという裏技を使ってみましょう。Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができます。

これを利用するには、アプリケーションのdevelopment環境の設定に手を加える必要があります。

development環境のメール設定
config/environments/development.rb
Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = false

  host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。
  # クラウドIDEの場合は以下をお使いください
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  # localhostで開発している場合は以下をお使いください
  # config.action_mailer.default_url_options = { host: host, protocol: 'http' }
  .
  .
  .
end

development環境のメール設定

config/environments/development.rb
Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = false

  host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。
  # クラウドIDEの場合は以下をお使いください
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  # localhostで開発している場合は以下をお使いください
  # config.action_mailer.default_url_options = { host: host, protocol: 'http' }
  .
  .
  .
end

もしローカル環境で開発している場合は、次のように変えるべきです。

host = 'localhost:3000'                     # ローカル環境
config.action_mailer.default_url_options = { host: host, protocol: 'http' }

リスト 11.18:アカウント有効化のプレビューメソッド(完成)
test/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    UserMailer.password_reset
  end
end

ユーザー登録を行うcreateアクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができます

app/controllers/users_controller.rb
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

authenticated?メソッドの抽象化

sendメソッドでメソッドを抽象化できます。

sendメソッドの例

$ rails console
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3

a = "attribute"
#{a}_digest
のように式展開できれば一番早いのですが、これでは文法エラーとなります。

したがって、sendメソッドを使用します。

app/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
  .
  .
  .
end

editアクションの実装

userが存在し、activatedではなく、認証に成功したら
→ログインを実行する。
という流れ。sendメソッドにレファクタリングしたので、authenticated?は引数が2つある。

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

有効でないユーザーがログインすることのないようにする

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
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

Userモデルにユーザー有効化メソッドを追加する

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

本番環境でのメール送信は、Herokuを使用していないのでスキップしました。

今回11章は一度で完全に理解することは出来なかったので、改めて見直したいと思います。
流れは理解できたので、後は自分でコードを書きながら試行錯誤していけば書けるかなと思います。

sendメソッドも自分なりに調べて、解釈してコードを描いてみる必要がありそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0