11章では、メールを使ったアカウントの有効化を実装します。
cookiesの実装と流れは似ているので、そんなに難しくはありません。
強いて言えば、mailerとsendメソッドを初めて使ったので少し戸惑いました。
ただ、mailerはcontrollerだと思えば、MVCの考え方に乗っ取っているので、分かりやすいです。
今回の流れは、以下の通りです。
(1)有効化トークンやダイジェストを関連付けておいた状態で
(2)有効化トークンを含めたリンクをユーザーにメールで送信し
(3)ユーザーがそのリンクをクリックすると有効化できるようにする
ポイントとしては、
・ユーザーの初期状態は「有効化されていない」(unactivated)にしておく。
・ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
・有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。
※editアクションの代わりに、showアクションでもよいがshowアクションは何かを表示するために使うのが一般的なので、他の開発者が意図を汲み取りづらいのでeditがベター。
ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated)に変更する。
トークン・ダイジェスト・認証の一覧
digestは後にsendメソッドでリファクタリングします。
1 | 2 | 3 | 4 |
---|---|---|---|
検索キー | パスワード・トークン | ダイジェスト | 認証 |
password | password_digest | authenticate(password) | |
id | remember_token | remember_digest | authenticated?(:remember, token) |
activation_token | activation_digest | authenticated?(:activation, token) | |
reset_token | reset_digest | authenticated?(:reset, token) |
AccountActivationscontrollerの作成
$ rails generate controller AccountActivations
routingの設定
今回は、editアクションしか使用しないので、only:[:edit]を記述
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
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
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というメソッドを探し、ユーザーを作成する前に実行するようになります。
サンプルユーザーの作成
# メインのサンプルユーザーを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メイラー
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
fromアドレスのデフォルト値を更新したアプリケーションメイラー
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環境のメール設定
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アクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができます
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メソッドを使用します。
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つある。
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
Userモデルにユーザー有効化メソッドを追加する
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
ユーザーモデルオブジェクトからメールを送信する
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
ユーザーモデルオブジェクト経由でアカウントを有効化する
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メソッドも自分なりに調べて、解釈してコードを描いてみる必要がありそうです。