はじめに
Railsチュートリアルを自分が理解しやすい表現に置き換える目的で利用しています。 手順を示す記事ではありません
目的
新規登録のユーザーに確認用メールを送り、有効化する
AccountActivationsソリース
注意
アカウントの有効化ステータスを変更するには本来PATCHリクエストのupdateアクションになるが、今回はMailerを使うので、有効化リンクをメールで送信することになる
つまり、ユーザーがリンクをクリックしブラウザに送られるアクションはGetリクエストになるので、eidtアクションを使う
有効化トークン
有効化トークンには、記憶トークンのような仮想的な属性を使ってハッシュ化した文字列をデータベースに保存するやり方をとる
user.activation_token
仮想属性の有効化トークンにアクセス
user.authenticated?(:activation, token)
ユーザーを認証する
ユーザーの有効をテストするために、activated属性を追加し論理値を取るようにする
if user.activated?
データモデルの追加
rails generate migration add_activation_to_users \
>activation_digest:string activated:boolean activated_at:datetime
生成されたマイグレーションがこちら
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
ここでは、activated属性のデフォルト値をfalseにしています(default: false)
before_create
6章でメールアドレスの作成及び更新の際にコールバックが呼ばれるようUserモデルにbefore_saveを定義しましたが、今回の場合では作成されたときだけ呼び出したいのでbefore_createを定義する
before_create :create_activation_digest
.
.
private
def create_activation_digest
end
create_activation_digestはUserモデル内でしか使わない為、privateで隠蔽しておきます
create_activation_digestメソッドでは、トークンに対応するダイジェストを割り当てる
attr_accessor :remember_token, :activation_token
.
.
private
def create_activation_digest
salf.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
attr_accessorに追加したのは、:remember_token同様、:activation_tokenも仮のものでなくてはならないから(攻撃を回避するために必要)
UserMailer
それでは有効化のメール送信に必要なActionMailerライブラリを使ってUserのメイラーを追加します
rails generate mailer UserMailer account_activation password_reset
これでtext用のビューとHtml用のビュー、Applicationメイラー
Userメイラーが生成される
user.mailer内にはアカウント有効化メソッド、パスワード再設定メソッドが自動で追加される
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout 'mailer'
end
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
@userのインスタンス変数はコントローラ同様、メイラーのビュー(text.erb , html.erb)でも利用できる
引数でuserを特定してそのアドレスへメールを送る
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) %>
注目すべきはこの一行
これはアカウント有効化するためのURLが生成している
edit_account_activation_url(@user.activation_token, email: @user.email)
今までを参考にすると
edit_user_url(user)
はusers/:id/editというURLが生成される
edit_account_activation_url(@user.activation_token, email: @user.email
ここでは、@user.activation_tokenを:idとみなしURLが生成され、その後にuserのアドレスを追加している
つまり、生成されるURLは**/account_activations/トークン/edit?メアド**となり
トークンはparams[:id]、メアドはparams[:email]で取得できる
:idの部分は実際にはnew_tokenのBase64で生成されているので
account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
となる
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
user変数にデータベース上の最初のユーザーを代入する
ユーザー登録を行う
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
ユーザーを作成するアクション
UserMailer.account_activation(@user).deliver_now
では、user_mailer.rbで定義した
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
メソッドを呼び出し、引数にとった@userにメールを送信する
redirect先がroot_urlなのは作成後にログインしないようにするためである
アカウントを有効化する
有効化するためには、アカウント有効化のダイジェストと渡されたトークンが一致するかチェックする
user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])
ただし、現時点ではremember_token用なので、正常に動作しない
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
sendメソッド
sendメソッドはシンボルや文字列など、受け取ったパラメータに応じてメソッドを呼び出せる便利なメソッドである
>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send("activation_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
これを使えば、先ほどのauthenticated?もremember_tokenだけじゃなく他でも使えそう
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
例えば、user.authencticated?(:activation, params[:id])としてeditアクションに組み込むと
第一引数の:activationはdigest = activation_digestとなり
第二引数のparams[:id]は有効化URLに含まれるトークンに値する
BCrypt::Password.new(digest).is_password?(token)
でdigestとトークンの比較を行う
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
editアクションでは、
userをメアドで取得し、userが存在 -> activated == false -> activation_digest == activation_tokenであれば実行
user.activatedをtrueに更新
user.activated_atを現在時刻へ更新
userでログイン
userページへリダイレクト
という流れになる。ちなみn
!user.activated?
は既に有効であるユーザーを再度有効化にしないために追加した論理値である
あとは有効でないユーザーがログインしないようにすればいい
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
if user.activated?を追加