【自分用】rails チュートリアル 12章

やること
パスワードを忘れた時のための再設定

12 パスワードの再設定
1 ユーザーがパスワードの再設定をリクエストすると、ユーザーのアドレスをキーにしてDBからユーザーを見つける
2 該当のメールアドレスがDBにある場合は、再設定用トークンとそれに対応する再設定ダイジェストを生成
3 再設定用ダイジェストはDBに保存しておき、再設定用トークンはアドレスと一緒にユーザーに送信する有効化メールのリンクに仕込んでおく
4 ユーザーがメールのリンクをクリックしたら、アドレスをキーにしてユーザーを探しDB内に保存しておいた再設定用ダイジェストと比較する（トークンを認証する）
5 認証に成功したら、パスわー変更用のフォームをユーザーに送信する

12.1 PasswordResetsリソース
PasswordResetsリソースのモデリングから
必要なデータ（再設定用のダイジェストなど）をUserモデルに追加

トピックブランチ作成
$ git checkout -b password-reset

12.1.1 PasswordResetsコントローラ
パスワード再設定用のコントローラ作成
前章と違い今回はビューも扱うのでnewアクションとeditアクションも生成
$ rails generate controller PasswordResets new edit --no-test-framework
コントローラの単体テストは生成せずに統合テストでカバーする

リスト12.1 ルーティングの用意
リスト12.2 ログインページにパスワード再設定画面へのリンク追加

演習
2 相対パスでなく絶対パス（https://〜）を使用するため

12.1.2 新しいパスワードの設定
アカウント有効化の時と同様トークン用の仮想的な属性とそれに対応するダイジェストを用意する
トークンはハッシュ化せずにDBに保存すると問題になる
必ずダイジェストを使用する
再設定用のリンクは短時間で期限切れになるようにする
→Userモデルにreset_digest属性とreset_sent_at属性を追加
$ rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime
$ rails db:migrate

リスト12.4 新しいパスワード設定のためメールアドレス入力画面
演習
1 password_resetモデルが無いため追加の情報追加の情報URLを渡す必要がある

12.1.3 createアクションでパスワード再設定
↑フォームから送信後、メールをアドレスをキーにしてユーザーをDBから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでDBの属性を更新する
その後、ルートURLにリダイレクト、フラッシュメッセージ表示
送信が無効な場合newページに飛ばしflashメッセージ表示

flash redirect_toと使用
flash.now renderと使用
flashは次の動作が終了するまで保持される
flash.nowは次の動作が開始された時点で消える

リスト12.5 password_resets_controllerにcreateアクション追加
リスト12.6 Userモデルにメソッド追加

12.2 パスワード再設定のメール送信
パスワードの再設定に関するメールの部分を作成

12.2.1 パスワード再設定のメールとテンプレート
リスト12.7 Userメイラーにpassword_resetメソッド作成
リスト12.8 テキストメールのテンプレート
リスト12.9 HTMLメールのテンプレート
リスト12.10 ブラウザからメールのプレビューができるようになる

12.2.2 送信メールのテスト
メイラーメソッドのテストを書いていく
リスト12.12 user_mailer_testにテスト追加

12.3 パスワードを再設定する
PasswordResetsコントローラのeditアクションを実装していく
統合テストも実装していく

12.3.1 editアクションで再設定
パスワード再設定フォームを表示するビュー作成
メールアドレスをキーにしてユーザーを探すのにeditアクションとupdateアクションの両方でメールアドレスが必要
メールアドレス入りリンクでeditアクションでは取り出せるが一度フォームを送信すると情報が消えてしまう
隠しフィールドを作成してページ内にメールアドレスを保存する
これにより他の情報と一緒にメールアドレスが送信される

リスト12.14 パスワード再設定フォーム
hidden_field_tag :email, @user.email
f.hidden_field :email, @user.email
前者はメールアドレスがparams[:email]に
後者はparams[:user][:email]に保存される
ひとつだけパラメータを他のアクションに渡したい時はhidden_field_tagを使う

今度は↑のフォームを描画するためにPasswordResetsコントローラのeditアクション内で@user変数を定義
params[:email]のメールアドレスに対応するユーザーを@userに保存する
params[:id]の再設定用トークンとauthenticated?メソッドを使って、ユーザーが存在するか、有効化されているか、認証されているか確認する
beforeフィルターでeditアクション前に実行させるようにする

リスト12.15 unlessは条件が全てtrueの時trueになり、どれかひとつだけでもfalseであればfalseを返す
つまり全てtrueであればeditアクションが実行され、falseがあればルートURLにリダイレクトされる
authenticated?(:reset, params[:id])
受け取ったユーザーIDの値をトークン化してダイジェスト化したものが:resetの値をダイジェスト化したものと一致したらtrue

12.3.2 パスワードを更新する
フォームからの送信に対応するupdateアクションが必要
1 パスワード再設定の期限が切れていないか
2 無効なパスワードであれば失敗させる(理由も表示させる)
3 新しいパスワードが空文字になっていないか
4 新しいパスワードが正しければ更新する

1への対応策
editとupdateアクションにbeforeフィルター追加
before_action :check_expiration, only: [:edit, :update] 
check_expirationメソッドは、有効期限をチェックするPrivateメソッドとして定義
password_reset_expired?メソッドはパスワード再設定の期限が切れている場合はtrueを返す Userモデルに追加

2,4への対応策
beforeフィルターで保護したupdateアクションを使う
2について 更新が失敗したときeditのビューが再描画される(flashメッセージも)
4について 更新が成功したときにパスワードを再設定してログインに成功した時と同じ処理を進める

3への対応策
Userモデルでは空でも良いとした。しかしパスワード欄が空では再設定できないので明示的に空であることをキャッチするコードを追加する
（confirmation確認欄が空のときはバリデーションで検出される）
@user.errors.add(:password, :blank)

以上のうち1のpassword_reset_expired?メソッド以外を実装
リスト12.16 password_resets_controllerのupdateアクション完成
リスト12.17 Userモデルにpassword_reset_expired?メソッド実装

12.3.3 パスワードの再設定をテストする
送信に失敗した場合と成功した場合の統合テスト作成
$ rails generate integration_test password_resets

forgot passwordフォーム表示
無効なメールアドレスを送信、次に有効なメールアドレス送信
後者ではパスワード再設定用のトークンが生成され再設定用メールが送信される
メールのリンクを開き、無効な情報を送信。次に有効な情報を送りきちんと動作しているかテストする

リスト12.18 パスワード再設定テスト
set up
deliveries変数の中身をクリア
@userにmichael代入

test password resets
password_resets#newにgetのリクエスト
password_resets/newが描画されているか

password_resets#createに無効なemail送信
flashがempty?がfalse→flashが表示されているか
password_resets/newが描画されているか

password_resets#createに有効なemail送信
引数の値が等しくない @user.reset_digestと@user.reload.reset_digest
引数の値が等しい 1とActionMailer::Base.deliveriesに格納された配列の数
flashがempty?がfalse→flashが表示されているか
root_urlにリダイレクト
userに@user代入(assignsメソッドで統合テストからはアクセスできないcontrollerのなかで定義されたインスタンス変数を参照する)

password_resets#editにgetリクエスト 有効なuser.reset_tokenと無効なemail
root_urlにリダイレクト
:activatedをtoggle!で反転 無効なユーザーに
password_resets#editにgetリクエスト 無効なuser.reset_tokenと無効なemail
root_urlにリダイレクト
:activatedをtoggle!で反転 有効なユーザーに戻す
password_resets#editにgetリクエスト 無効なuser.reset_tokenと有効なemail
root_urlにリダイレクト
password_resets#editにgetリクエスト 有効なuser.reset_tokenと有効なemail
password_resets/editが描画される

指定したHTMLタグが存在するか "input[name=email][type=hidden][value=?]", user.email
user.reset_tokenを引数にしたpassword_reset_pathにpatchリクエスト、有効なemailと異なるパスワードと確認欄
指定したHTMLタグが存在するか 　div id="error_explanation"
user.reset_tokenを引数にしたpassword_reset_pathにpatchリクエスト、有効なemailと空のパスワードと確認欄
指定したHTMLタグが存在するか 　div id="error_explanation"
user.reset_tokenを引数にしたpassword_reset_pathにpatchリクエスト、有効なemailと有効なパスワードと確認欄
trueかどうか テストユーザーがログインした
flashがempty?がfalse→flashが表示されているか
userページにリダイレクト

演習
1 update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)

2 assert_match /expired/i, response.body
リダイレクトされたページに/expired/iが含まれているか

3 @userの:reset_digestの値をnilに更新して保存

4 assert_nil user.reload.reset_digest
再読み込みしたuserのreset_digestがnilであればtrue