演習1
この時点で、テストスイートが green になっていることを確認してみましょう。
rails testでGREENになることを確認してみてください
表 12.1の名前付きルーティングでは、_pathではなく_urlを使っているのはなぜでしょうか?理由を考えてみましょう。(ヒント: アカウント有効化の演習11.1.1.1と同じ理由です。)
https://qiita.com/ryoheitakahashi/items/5daa27cb3dec4690852f
こちらを参考にしてみてください
演習2
リスト 12.4のform_withメソッドで、@password_resetではなく:password_resetを使っている理由を考えてみましょう。
そもそも@password_resetで定義されたインスタンス変数が定義されていないため
演習3
試しに有効なメールアドレスをフォームで送信してみましょう(図 12.6)。どんなエラーメッセージが表示されますか?
エラーになります。私の場合は以下のエラーが出ました
上の演習課題で送信した結果はエラーと表示されますが、該当するuserオブジェクトにはreset_digestとreset_sent_atが存在することをコンソールで確認してみましょう。それぞれの値はどのようになっていますか?
User.find_by(email: "example@railstutorial.org")
user.reset_digest
=> "$2a$12$O6oFLkLe8Cc81C0VCQPi2.sDODGvr801KmgmZ0G/AgSffQ1xz6FqO"
user.reset_sent_at
=> Tue, 03 Oct 2023 20:58:27.716286000 UTC +00:00
演習4
送信メールをブラウザでプレビューしてみましょう。「Date」フィールドにはどんな情報が表示されていますか?
現在時刻が示されています(現地時間)
パスワード再設定フォームで有効なメールアドレスを送信してみましょう。また、Railsサーバーのログを見て、生成された送信メールの内容を確認してみてください。
Railsコンソールを開いて、上の演習課題でパスワードを再設定したUserオブジェクトを探してください。オブジェクトを見つけたら、そのオブジェクトが持つreset_digestとreset_sent_atの値を確認してみましょう。
id: 116,
name: "Test",
email: "example@gmail.com",
created_at: Tue, 03 Oct 2023 19:42:11.244258000 UTC +00:00,
updated_at: Wed, 04 Oct 2023 20:13:34.847753000 UTC +00:00,
password_digest: "$2a$12$4tHjMhl7ELeIYGIsRPCQP.7vlIB7.TWeC7KbOE9nuD0SIk8U4.oY6",
remember_digest: nil,
admin: false,
activation_digest: "$2a$12$GgX33eTJ0265.qQuLOPXt.iRA8tbjSY50njvtu8LfdQY2QD32Yw1q",
activated: false,
activated_at: nil,
reset_digest: "$2a$12$DyjP6757wCv82BRHgdC4XesYCnVGO3fm4xyFYQPgPi18fsRD9lrfa",
reset_sent_at: Wed, 04 Oct 2023 20:13:34.847375000 UTC +00:00>
演習5
メーラーのテストだけを実行してみてください。このテストは green になりますか?
rails test:mailers
Started with run options --seed 33026
2/2: [===================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.17131s
2 tests, 16 assertions, 0 failures, 0 errors, 0 skips
リスト 12.12にある2つ目のCGI.escapeメソッドを削除すると、テストが red になることを確認してみましょう。
CGI.escape(user.email),の部分を削除して、REDになることを確認しましょう
演習6
演習12.2.1.1で示した手順に従って、Railsサーバーのログから送信メールを探し出し、そこに記されているパスワード再設定用リンクを見つけてください。そのリンクをブラウザで表示すると、図 12.11のようになることを確かめてみましょう。
<p>To reset your password click the link below:</p>
<a href="https://<hex string>.app.github.dev/password_resets/VNajJPu5yhxnnOvuV8yKtw/edit?email=example%40gmail.com">Reset password</a>
<p>This link will expire in two hours.</p>
この部分の
/password_resets/VNajJPu5yhxnnOvuV8yKtw/edit?email=example%40gmail.com">Reset password |
---|
をrailsのURLの後ろに付けましょう
http://127.0.0.1:3000/password_resets/VNajJPu5yhxnnOvuV8yKtw/edit?email=example%40gmail.com |
---|
みたいな感じ
※補足
私の場合、正しいユーザーと認識されず、ブラウザ上で正しいURLを入力してもrootへリダイレクトされてしまいました
もし同じ現象にあったら
controllers/password_resets_controller.rb
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
# redirect_to root_url
end
end
end
redirect_to root_urlをコメントアウトしてみましょう
※後々コメントアウトで放置するとエラーの原因になるので、適宜コメントアウトは外しましょう
上で表示したページから、実際に新しいパスワードを送信してみましょう。どのような結果になりましたか?
私の場合は何も起きず、リダイレクトされてしまいました
演習7
演習12.2.1.1でRailsサーバーのログから取得できるリンクをブラウザで表示し、passwordフィールドとconfirmationフィールドの文字列をわざと間違えて送信してみましょう。どんなエラーメッセージが表示されますか?
何でも良いので失敗してみましょう。下記画像では、短すぎるパスワード、passwordとconfirmationが異なる入力をしてみました
Railsコンソールを開いて、パスワード再設定を送信したユーザーオブジェクトを見つけてください。見つかったら、そのオブジェクトのpassword_digestの値を取得してみましょう。次に、パスワード再設定フォームから有効なパスワードを入力し、送信してみましょう(図 12.13)。パスワードの再設定が成功したら、再度password_digestの値を取得し、先ほど取得した値と変わっていることを確認してみましょう。(ヒント: 新しい値はuser.reloadを実行してから取得する必要があります。)
まずは再設定を送信したユーザーを見つけましょう
User Update (0.2ms) UPDATE "users" SET "updated_at" = ?, "password_digest" = ? WHERE "users"."id" = ? [["updated_at", "2023-10-06 20:56:23.952401"], ["password_digest", "$2a$12$bu5eUhGUji5ZYRGeRofEUe.xo6K8.LRPCAqos4Y/81DFb5jeyHhFq"], ["id", 116]]
次にrails consoleで先ほどのユーザーのpassword_digestを取得します
先ほどのpassword_digestとコンソールのdigestが一致していることを確認しましょう
User.find_by(id:116)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 116], ["LIMIT", 1]]
=>
#<User:0x00007f67e4859758
id: 116,
name: "Test",
email: "example@gmail.com",
created_at: Tue, 03 Oct 2023 19:42:11.244258000 UTC +00:00,
updated_at: Fri, 06 Oct 2023 20:56:24.209526000 UTC +00:00,
password_digest: "$2a$12$bu5eUhGUji5ZYRGeRofEUe.xo6K8.LRPCAqos4Y/81DFb5jeyHhFq",
remember_digest: "$2a$12$OSrsG8q06dojTth764zQNu/9aS.e49JSEHaKk.Jnalbsm5.oYsosi",
admin: false,
activation_digest: "$2a$12$GgX33eTJ0265.qQuLOPXt.iRA8tbjSY50njvtu8LfdQY2QD32Yw1q",
activated: false,
activated_at: nil,
reset_digest: "$2a$12$/p7vH.IW.xNGP1i2468P5eFjtYJ8YO6Tw55ZXuNtsVDYZHf57mrGK",
最後にパスワードの再設定を成功させ、改めてpassword_digestを確認しましょう
値が変わっていれば、OKです
User Update (0.2ms) UPDATE "users" SET "updated_at" = ?, "password_digest" = ? WHERE "users"."id" = ? [["updated_at", "2023-10-06 21:04:54.547381"], ["password_digest", "$2a$12$8cUVGSNVLLkhel980.hQIuvm/B/7GQWugrcy0j0icsiqBsGrlVuFq"], ["id", 116]]
演習9.3.2.1では、セッションハイジャック(9.1.1)から保護するためのセッショントークンを実装しました。想定されるシナリオの1つとして「セッションを盗まれたことに気づいたユーザーが即座にパスワードをリセットする」という状況が考えられます。特に、ハイジャックされたセッションをこの操作で自動的に失効させることができたら素晴らしいでしょう。この機能を実現するのに必要なコードをリスト 12.18の(コードを書き込む)の部分に書いてください。(ヒント: リスト 9.37で、記憶ダイジェストをセッショントークンとして再利用したこと、Userモデルにはリスト 9.11で示した記憶トークンを削除するメソッドが既にあることを思い出しましょう。)
app/controllers/password_reset.html.erb
def update
if params[:user][:password].empty?
@user.errors.add(:password, "can't be empty")
render 'edit', status: :unprocessable_entity
elsif @user.update(user_params)
@user.update_attribute(:reset_digest, nil)
reset_session
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit', status: :unprocessable_entity
end
end
演習8
リスト 12.6にあるcreate_reset_digestメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 12.21に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります。また、変更後にテストを実行し、 green になることも確認してください。ちなみにリスト 12.21にあるコードには、前章の演習(リスト 11.40)の解答も含まれています。(注: update_columnsはバリデーションが実行されない上、update_attributeと異なりモデルのコールバックも行われないため、本チュートリアル以外で使用する際は注意が必要です。)
app/models/user/rb
def create_reset_digest
self.reset_token =User.new_token
update_columns(reset_digest: User.digest(reset_token),reset_sent_at: Time.zone.now)
end
リスト 12.22のテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐(リスト 12.16)を統合テストでカバーしてみましょう。リスト 12.22 のコードにあるresponse.bodyは、そのページのHTML本文をすべて返すメソッドです。期限切れをテストする方法はいくつかありますが、リスト 12.22でオススメした手法を使えば、レスポンスの本文に「expired」という語があるかどうかでチェックできます(なお、大文字と小文字は区別されません)。
test/integration/password_resets_test.rb
class ExpiredTokenTest < ExpiredToken
test "should redirect to the password-reset page" do
assert_redirected_to new_password_reset_url
end
test "should include the word 'expired' on the password-reset page" do
follow_redirect!
assert_match /expired/i, response.body
end
end
end
2時間経過するとパスワードを再設定できないようにする機能はセキュリティ的に好ましい手法ですが、公共の場所に設置されているコンピュータや、複数のユーザーが共有するコンピュータが使われる可能性も考慮すれば、より安全性の高いセキュリティ対策にするのが望ましいでしょう。公共の場所で共有されるコンピュータの多くは誰でもログインできるので、あるユーザーがそのコンピュータから離席するときに正しくログアウトしていたとしても、別のユーザーが2時間以内にそのコンピューターにログインし、ブラウザの「戻る」ボタンを数回押してパスワード再設定フォームを見つけたら、パスワード更新に成功してしまう可能性があり、しかもそのままログインされてしまいます。この問題を解決するために、リスト 12.23のコードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょう6。
app/controllers/pssword_resets_controller.rb
def update
if params[:user][:password].empty?
@user.errors.add(:password, "can't be empty")
render 'edit', status: :unprocessable_entity
elsif @user.update(user_params)
log_in @user
@user.update_attribute(:reset_digest, nil)
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit', status: :unprocessable_entity
end
end
リスト 12.19に1行追加し、上の演習課題に対するテストを書いてみましょう。(ヒント: リスト 9.26のassert_nilメソッドとリスト 11.34のuser.reloadメソッドを組み合わせて、reset_digest属性を直接テストしてみましょう。)
ちょっと自信なし
test/integration/password_restes_test.rb
test "update with valid password and confirmation" do
patch password_reset_path(@reset_user.reset_token),
params: { email: @reset_user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" } }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to @reset_user
assert_nil @user.reload.reset_digest
end