3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsチュートリアル12章備忘録

Last updated at Posted at 2021-03-24

##概要
チュートリアル2周目の自分へ向けてさらっと
なにやったか要点だけおさえておく。

##パスワードの再設定

$ git checkout -b password-reset

この章でやること

  • ログインページのformに「forgot password」リンクを追加
  • 「forgot password」formのページ作成
  • 「Reset password」パスワード再設定の確認を求めるformのページ作成

全体の流れ

  1. ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてDBからユーザーを見つける

  2. 該当のメールアドレスがDBにある場合は、再設定用トークンとそれに対応する再設定ダイジェストを生成する

  3. 再設定用ダイジェストはDBに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく

  4. ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、DB内に保存しておいた再設定用ダイジェストと比較する(トークンを認証する)

  5. 認証に成功したら、パスワード変更用のformをユーザーに表示する

##PasswordResetsリソース
新たなモデルは作らずに、代わりに必要なデータ(再設定用のダイジェストなど)をUserモデルに追加していく。

###PasswordResetsコントローラ
最初のステップとしてパスワード再設定用のコントローラを作成
$ rails generate controller PasswordResets new edit --no-test-framework

no-test -> テストを生成しないオプション
コントローラの単体テストをする代わりに、統合テストでカバーしていくため。

今回の実装では、新しいパスワードを再設定するためのフォームと、
Userモデル内のパスワードを変更するためのフォームが必要になる。
new、create、edit、updateのルーティングも用意する。

config/routes.rb
  resources :password_resets,     only: [:new, :create, :edit, :update]
app/views/sessions/new.html.erb
      <%= 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

app/views/sessions/new.html.erb(再掲載)
      <%= 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
パスワード再設定に対しても行う

####パスワード再設定のリンクをメール送信

app/mailers/user_mailer.rb
  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のメールプレビュー機能でパスワード再設定のメールをプレビューするメソッド

test/mailers/previews/user_mailer_preview.rb
  # 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でパスワード再設定メールを確認。

###送信メールのテスト
メーラーメソッドのテストを書く

test/mailers/user_mailer_test.rb
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アクションでメールアドレスを取り出すことは問題ないが、フォームを一度送信してしまうと、この情報は消えてしまう。
-> ページ内に隠しフィールドとして保存する手法をとれば、フォームから送信したときに他の情報と一緒にメールアドレスが送信される。

app/views/password_resets/edit.html.erb
      <%= hidden_field_tag :email, @user.email %>

###パスワードを更新する
formから新しいパスワードを送信するには、formからの送信に対応するupdateアクションが必要。
このupdateアクションでは、次の4つのケースを考慮する必要がある。

  1. パスワード再設定の有効期限が切れていないか
  2. 無効なパスワードであれば失敗させる(失敗した理由も表示する)
  3. 新しいパスワードが空文字列になっていないか(ユーザー情報の編集ではOKだった)
  4. 新しいパスワードが正しければ、更新する

(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時間以上パスワードが再設定されなかった場合は期限切れとする処理を行う。

app/models/user.rb
  # パスワード再設定の期限が切れている場合は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

test/integration/password_resets_test.rb
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
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?