##概要
チュートリアル2周目の自分へ向けてさらっと
なにやったか要点だけおさえておく。
##パスワードの再設定
$ git checkout -b password-reset
この章でやること
- ログインページのformに「forgot password」リンクを追加
- 「forgot password」formのページ作成
- 「Reset password」パスワード再設定の確認を求めるformのページ作成
全体の流れ
-
ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてDBからユーザーを見つける
-
該当のメールアドレスがDBにある場合は、再設定用トークンとそれに対応する再設定ダイジェストを生成する
-
再設定用ダイジェストはDBに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
-
ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、DB内に保存しておいた再設定用ダイジェストと比較する(トークンを認証する)
-
認証に成功したら、パスワード変更用のformをユーザーに表示する
##PasswordResetsリソース
新たなモデルは作らずに、代わりに必要なデータ(再設定用のダイジェストなど)をUserモデルに追加していく。
###PasswordResetsコントローラ
最初のステップとしてパスワード再設定用のコントローラを作成
$ rails generate controller PasswordResets new edit --no-test-framework
no-test
-> テストを生成しないオプション
コントローラの単体テストをする代わりに、統合テストでカバーしていくため。
今回の実装では、新しいパスワードを再設定するためのフォームと、
Userモデル内のパスワードを変更するためのフォームが必要になる。
new、create、edit、update
のルーティングも用意する。
resources :password_resets, only: [:new, :create, :edit, :update]
<%= link_to "(forgot password)", new_password_reset_path %>
| HTTPリクエスト | URL | Action | 名前付きルート |
|:-:|:-:|:-:|:-:|:-:|
|GET| /password_resets/new | new | new_password_reset_path |
|POST| /password_resets | create | password_resets_path |
|GET| /password_resets/トークン/edit | edit | edit_password_reset_url(token) |
|PATCH| /password_resets/トークン | update | password_reset_url(token) |
new_password_reset_path
は、RESTfulのルーティングに従っている。
###新しいパスワードの設定
Userモデルにパスワード再設定用のカラムを追加
users | 型 |
---|---|
reset_digest | string |
reset_sent_at | datetime |
マイグレーションに属性を追加 | |
$ rails generate migration add_reset_to_users reset_digest:string \ |
|
> reset_sent_at:datetime |
$ rails db:migrate
<%= link_to "(forgot password)", new_password_reset_path %>
###createアクションでパスワード再設定
-
formから送信を行なった後、メールアドレスをキーとしてユーザーをDBから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでDBの属性を更新する必要がある。
-
その後ルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示。送信が無効の場合は、ログインと同様にnewページを出力してflash.nowメッセージを表示する。
##パスワード再設定のメール送信
Userメーラー(app/mailers/user_mailer.rb
)を生成したときに、
デフォルトのpassword_resetメソッドもまとめて生成されている。
###パスワード再設定のメールとテンプレート
UserメイラーにあるコードをUserモデルに移すリファクタリング
UserMailer.password_reset(self).deliver_now
パスワード再設定に対しても行う
####パスワード再設定のリンクをメール送信
def password_reset(user)
@user = user
mail to: user.email, subject: "Password reset"
end
####パスワード再設定のテンプレート(テキスト)作成
app/views/user_mailer/password_reset.text.erb
####パスワード再設定のテンプレート(HTML)作成
app/views/user_mailer/password_reset.text.erb
####Railsのメールプレビュー機能でパスワード再設定のメールをプレビューするメソッド
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end
railsサーバーのlogでパスワード再設定メールを確認。
###送信メールのテスト
メーラーメソッドのテストを書く
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "password_reset" do
user = users(:michael)
user.reset_token = User.new_token
mail = UserMailer.password_reset(user)
assert_equal "Password reset", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.reset_token, mail.body.encoded
assert_match CGI.escape(user.email), mail.body.encoded
end
end
##パスワードを再設定する
PasswordResetsコントローラのeditアクションの実装を進める。
###editアクションで再設定
パスワード再設定の送信メールには、次のようなリンクが含まれている
https://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=fu%40bar.com
このリンクを機能させるには、パスワード再設定フォームを表示するビュー
(パスワード入力フィールドと確認用フィールド)が必要。
面倒な点
-> メールアドレスをキーとしてユーザーを検索するためには、editアクションとupdateアクションの両方でメールアドレスが必要。
editアクションでメールアドレスを取り出すことは問題ないが、フォームを一度送信してしまうと、この情報は消えてしまう。
-> ページ内に隠しフィールドとして保存する手法をとれば、フォームから送信したときに他の情報と一緒にメールアドレスが送信される。
<%= hidden_field_tag :email, @user.email %>
###パスワードを更新する
formから新しいパスワードを送信するには、formからの送信に対応するupdateアクションが必要。
このupdateアクションでは、次の4つのケースを考慮する必要がある。
- パスワード再設定の有効期限が切れていないか
- 無効なパスワードであれば失敗させる(失敗した理由も表示する)
- 新しいパスワードが空文字列になっていないか(ユーザー情報の編集ではOKだった)
- 新しいパスワードが正しければ、更新する
(1)はeditとupdateアクションに次のようなメソッドとbeforeフィルターを用意することで対応する。
before_action :check_expiration, only: [:edit, :update]
#期限切れかどうかを確認する
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
password_reset_expired?
-> 期限切れかどうかを確認するインスタンスメソッド。Userモデルで定義する。
2時間以上パスワードが再設定されなかった場合は期限切れとする処理を行う。
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
(2)更新が失敗したときにeditのビューを再描画させ、
<%= hidden_field_tag :email, @user.email %>
にエラーメッセージが表示させ解決。
(4)更新が成功したときにパスワードを再設定し、あとはログインに成功したときと同様の処理を進めて解決。
(3)今回の問題点は、パスワードが空文字だった場合の処理。
理由は以前Userモデルを作った時に、パスワードが空でも良い(allow_nil
)実装をしたため。
今回は@userオブジェクトにerrors.add
を使ってエラーメッセージを追加する。
@user.errors.add(:password, :blank)
-> パスワードが空だった時に空の文字列に対するデフォルトのメッセージを表示してくれる。
app/controllers/password_resets_controller.rb
にあてはめる。
##パスワードの再設定をテストする
送信に成功した場合と失敗した場合の統合テストを作成する。
$ rails generate integration_test password_resets
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
assert_select 'input[name=?]', 'password_reset[email]'
# メールアドレスが無効
post password_resets_path, params: { password_reset: { email: "" } }
assert_not flash.empty?
assert_template 'password_resets/new'
# メールアドレスが有効
post password_resets_path,
params: { password_reset: { email: @user.email } }
assert_not_equal @user.reset_digest, @user.reload.reset_digest
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
# パスワード再設定フォームのテスト
user = assigns(:user)
# メールアドレスが無効
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
# 無効なユーザー
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# メールアドレスが有効で、トークンが無効
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# メールアドレスもトークンも有効
get edit_password_reset_path(user.reset_token, email: user.email)
assert_template 'password_resets/edit'
assert_select "input[name=email][type=hidden][value=?]", user.email
# 無効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "barquux" } }
assert_select 'div#error_explanation'
# パスワードが空
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "",
password_confirmation: "" } }
assert_select 'div#error_explanation'
# 有効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" } }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user
end
end