##はじめに
Railsチュートリアル11章の内容を、少しでも理解の助けとなればと思い、割としっかり目に整理しました!
備忘録です。
##前提
Railsチュートリアル1〜10章までの内容が完了していること。
##内容
Railsチュートリアル11章の__アカウント有効化機能の実装手順__を、前中後半の3回に分けて整理しました。
後編である今回は、__アカウント有効化機能の実装__をしていきます!
前編→AccountActivationsリソースの作成
中編→アカウント有効化メール送信機能の実装
##3.アカウントを有効化する
●AccountActivationsコントローラのeditアクションに、アカウント有効化機能を実装する処理を記述していく。
●editアクションへのテストを記述し、パスさせる。
●テストがパスしたら、AccountActivationsコントローラ内の記述をUserモデルへ移していくリファクタリングを行っていく。
####3-1.ユーザーを検索して認証する処理を記述していく。
●__アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかを判定する条件式を記述する__。
●__パスワードの判定や記憶トークンの判定でも使用した、authenticated?メソッドを使っていくことにする__。
➡︎早速、有効化ダイジェストと有効化トークンの一致を判定する用のauthenticated?メソッドを定義していきたいところだが、このまま作成してしまうと、authenticated?メソッドが3種類存在することになり、重複する記述が増えることになる。
➡︎じゃあどうするかと言うと、その重複する部分は抽象化して一挙にまとめるようにする。
→具体的には、送信されるパラメータを各パラメータ毎に取得できるように、引数を変数化する。
→つまり、記憶トークンが送信されたら"記憶パラメータ"を、有効化トークンが送信されれば"有効化パラメータ"を取得できるようにする。
➡︎パラメータ毎に取得させるためには、Rubyが有する「メタプログラミン」という「プログラムでプログラムを作成する」手法を用いる。
→そのためにsendメソッドを使う。何故sendメソッドを使うのか、コンソールを開いて実際に試してみるとわかりやすい。
$ rails console --sandbox
user = User.first
user.activation_digest
=>"abcdefg"(ランダムで作成されたトークン)
user.send(:activation_digest)
=> "abcdefg"
user.send("activation_digest")
=> "abcdefg"
hoge = :activation
user.send("#{hoge}_digest")
=> "abcdefg"
全部、user.activation_digestを呼ぶのと同じ処理になる。
➡︎上記のように、sendメソッドを呼び出すと渡す引数が文字列、シンボル、文字列の式展開であっても、オブジェクトに対して呼び出す事ができる。
➡︎このsendメソッドを利用して、authenticated?メソッドを書き換えていく。
→まず、authenticated?メソッドの第一引数ではパラメータ、第二引数ではトークンを取得するよう引数を抽象化する。
→authenticated?(attribute, token)
→取得したパラメータを、sendメソッドを使ってそれぞれの属性へ変数化。
→digest = send("#{attribute}\_digest")
→これにより、あらゆるダイジェストとトークンの一致による認証が可能になった。
→例えば、user.authenticated?(:remember, remember_token)などとすれば、記憶ダイジェストと記憶トークンの比較ができる。
➡︎ただしこのままでは、テストがREDになる。理由は2つ。
1つ:sessions_helperファイルのcurrent_userメソッド内の、authenticated?メソッドを抽象化しなければならない。
2つ:Userテスト内のauthenticated?メソッドを抽象化しなければならない。
➡︎上記2つを書き換えることにより。テストがGREENとなるはず。
>```ruby:app/models/user.rb
class User < ApplicationRecord
.
.
.
#authenticated?メソッドを抽象化する。
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
.
.
.
end
app/helpers/sessions_helper.rb
def current_user
.
.
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
#current_user内でも抽象化authenticated?メソッドに書き換える。
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
>```ruby:test/models/user_test.rb
def setup
@user = User.new...
end
.
.
.
#抽象化したauthenticated?メソッドの記述の仕方に書き換える。
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?(:remember, '')
end
####3-2.editアクションでユーザーのアカウントを有効化する機能を実装する。
●authenticated?メソッドを抽象化して、有効化ダイジェストと有効化トークンを比較できるようになったので、editアクションにauthenticated?メソッドを追加し、editアクションを完成させてきましょ。
➡︎まずeditアクションに、まずはparamsハッシュで取得する処理を記述する。
→user = User.find_by(email: params[:email])
➡︎そのメールアドレスに対応するユーザーを認証する処理を記述する。
→if user && user.authenticated?(:activation, params[:id])
➡︎ただしこのままでは第三者によってリンクを盗まれ、そのままログインされてしまうというセキュリティ上の問題が存在する。
➡︎そしてこの問題を解決するには、一度有効化したアカウントについては、それ以降有効化しないという条件を追加しなければならない。
➡︎この条件を追加すれば、一度ユーザーが自らのアカウントを有効化してしまえば、後からリンクを盗まれたとしてもログインされないで済む。
→if user && !user.activated? && user.authenticated?(:activated, params[:id])
→!user.activated?と言う部分で「有効化されていなければtrueを返す」条件式を追加している。
➡︎条件式の結果ユーザーを認証できたら、ユーザーの認証データを更新する処理を記述。
→user.update_attribute(:activated, true)
→user.update_attribute(:activated_at, Time.zone.now)
➡︎そしてユーザーをログインさせ、フラッシュをビューに表示し、ユーザー詳細ページへリダイレクトさせる記述をする。
➡︎滅多に起こることはないが、トークンが無効になってしまった場合の処理も記述してあげる。
→フラッシュを表示させ、ルートURLへリダイレクトさせる。
➡︎
●次にログイン方法の処理にも変更を加えていく。ログインする際に有効なユーザーの場合のみ、ユーザーをログインさせるようにする。
➡︎どうすればいいかというと、user.activated?の値がtrueの場合にのみログインを許可させる処理を記述すればいい
➡︎また、それ以外の場合はルートURLへリダイレクトさせて、フラッシュでキーをwarningにして警告文を表示させる。
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] = "アカウントが有効化されました"
redirect_to user
else
flash[:danger] = "無効なリンクです"
redirect_to root_url
end
end
end
>```ruby:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:session][:email])
if user && user.authenticate(params[:session][:password])
#有効でないユーザーはログインさせないようにする。
if user.activated?
log_in user
params[:session][:remember_token] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "アカウントが有効化されていません"
message += "アカウント有効化用のリンクを確認してください"
flash[:warning] = message
redirect_to root_url
end
else
.
.
.
end
end
####3-3.有効化のテストとリファクタリング
●ユーザー登録した際に、アカウントが有効化されたかどうかを確認する統合テストを書いていきましょ。
➡︎ユーザー登録時のテストはすでに記述してあるので、そこに記述を追加していく。
➡︎テストファイル内の上部にsetupメソッドを用意して、テスト用にメイラーに対して空の配列を作成する。
→ActionMailer::Base.deliveries.clear
→配列deliveriesは変数であり、setupメソッドにて初期化処理をしておかないと、並行して行われる他のテストで、メールを配信させる処理を記述した際にエラーが発生してしまうので注意。
➡︎有効化されたアカウントで新規登録するテストを記述していく。
→GETリクエストで新規登録画面へ移動。
→有効なユーザー情報をPOSTリクエストした際に、ユーザー総数に+1の差異があることを検証する記述を追加。
→登録が完了したことにより、メイラーの配列の値に1つデータが追加され、データ数が1つになったことを検証する記述を追加。
→createアクションから直接ユーザーのインスタンス変数を参照し、取得する。
→ユーザーが有効化されていない事を検証する記述を追加。
→有効化されていない状態でログインする記述を追加。
→ログインユーザーが存在しない事を検証する記述を追加。
→不正な有効化トークンをGETリクエストで送信した場合の記述を追加。
→ログインユーザーが存在しない事を検証する処理の記述。
→トークンは有効だが、無効なメールアドレスをGETリクエストで送信した場合の記述を追加。
→ログインユーザーが存在しない事を検証。
→正しい有効化トークンとメアドを取得した場合の処理を記述する。
→DBを更新して、ユーザーが有効化されたかどうかを検証する記述を追加。
→ページを移動する。
→チェンジ先のビューテンプレがユーザーページである事を検証する記述を追加、
→ログインユーザーが存在する事を検証する記述を追加。
➡︎テストスイートGREEN。
➡︎ユーザー操作の一部を、コントローラからモデルに移動させるリファクタリングを行う準備が完了。
●Userモデルにユーザー有効化メソッドを定義する。
➡︎app/models/user.rbファイルへ移動。
➡︎ユーザーの有効化属性を更新するメソッドを定義する。
→メソッド名をactiveとする。
→activated属性をtrueへ更新し、有効化する処理を記述
→activated_at属性を、現在の時刻へと更新する処理を記述する。
→Userモデルに、user変数を定義していないので、user.としないよう注意する事。
➡︎有効化用のメールを送信するメソッドを定義する。
→メソッド名をsend_activateion_emailとする。
→Userメイラーを使い、アカウント有効化メールを送信する処理を記述する。
●Usersコントローラのcreateアクション内で、ユーザーオブジェクトからメールを送信させる処理を記述する。
➡︎ユーザーが保存された後に送信されるよう処理を記述する。
●AccountActivationsコントローラのeditアクション内で、ユーザーモデルオブジェクト経由でアカウントを有効化する。
➡︎ユーザー認証完了後に、アカウントが有効化されるようする処理を記述する。
●リファクタリングが正しく行えていれば、テストはパスする。
test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
#ユーザー登録のテストに、アカウント有効化のテストを追加する。
def setup
ActionMailer::Base.deliveries.clear
end
.
.
.
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: {user: {name: "有効な名前",
email: "有効なメアド",
password: "有効なパスワード",
password_cinfirmation: "有効なパスワード確認"}}
end
#有効なユーザーデータをPOSTリクエストして、メイラーが1つ増える。
assert_equal 1, ActionMailer::Base.deliveries.size
user = assigns(:user)
assert_not user.activated?
#有効化していない状態でログイン
log_in_as(user)
assert_not is_logged_in?
#不正なトークンでリンクへアクセス
get ediit_account_activation_path("不正なトークン", email: user.email)
assert_not is_logged_in?
#不正なメアドでアクセス
get ediit_account_activation_path(user.activation, email: "不正なメアド")
assert_not is_logged_in?
#有効化トークンが正しい場合にアクセス
get edit_accoount_activation_path(user.activation_token, email: user.email)
#アクセスに成功してページ遷移
follow_redirect!
#ユーザーのプロフページが表示されている事を検証
assert_template 'users/show'
#ログインできてることを検証
assert is_logged_in?
end
end
>```ruby:app/models/user.rb
class User < ApplicationRecord
.
.
.
#Userモデルにアカウント有効化するメソッドを追加。
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
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
>```ruby: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
##最後に
後編の__アカウント有効化機能の実装__はここまでになります。(演習については割愛)
前編→AccountActivationsリソースの作成
中編→アカウント有効化用メール送信機能の実装
##参考
Railsチュートリアル 第11章アカウントの有効化