学習日記

Ruby on Rails勉強してます

今日やったこと12

第12章 パスワードの再設定

全体の流れ

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

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

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

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

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

12.1 PasswordResetsリソース

トピックブランチを作成。

$ git checkout -b password-reset

パスワード再設定用のコントローラを作る。その際、newアクションとeditアクションも一緒に生成する。

$ rails generate controller PasswordResets new edit --no-test-framework

ルーティングを設定。

config/routes.rb

Rails.application.routes.draw do
  ・
  ・
  ・
  resources :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
end
HTTPリクエス URL Action 名前付きルート
GET /password_reset/new new new_password_resets_path
POST /password_reset create password_resets_path
GET /password_reset/(token)/edit edit edit_password_resets_url(token)
PATCH /password_reset/(token) update password_resets_url(token)

パスワード再設定画面へのリンクを追加する。

app/views/sessions/new.html.erb

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= link_to "(forgot password)", new_password_reset_path %> <!-- リンクを追加 -->
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

reset_digest属性とreset_sent_at属性をUserモデルに追加する。
下記を実行して、マイグレーションに属性を追加。

$ rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime
$ rails db:migrate

新しいパスワード再設定画面ビューを作成する。

app/views/password_resets/new.html.erb

 <% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "Submit", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

フォームから送信後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要がある。続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示する。送信が無効の場合は、ログインと同様にnewページを出力してflash.nowメッセージを表示する。

app/controllers/password_resets_controller.rb

class PasswordResetsController < ApplicationController

  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

  def edit
  end
end

Userモデルにパスワード再設定用メソッドを追加。

app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  before_save   :downcase_email
  before_create :create_activation_digest
  .
  .
  .

  # パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  private

  .
  .
  .

end


12.2 パスワード再設定のメール送信

Userメイラーのpassword_resetメソッドを変更し、テキストメールのテンプレートとHTMLメールのテンプレートもそれぞれ変更する。

app/mailers/user_mailer.rb

class UserMailer < ApplicationMailer

  ・
  ・
  ・

  def password_reset(user)
    @user = user
    mail to: user.email, subject: "Password reset"
  end
end

app/views/user_mailer/password_reset.text.erb


To reset your password click the link below:

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

This link will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password will stay as it is.

app/views/user_mailer/password_reset.html.erb

<h1>Password reset</h1>

<p>To reset your password click the link below:</p>

<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
                                                      email: @user.email) %>

<p>This link will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>

Railsのメールプレビュー機能でパスワード再設定のメールをプレビューするためにコードを追加する。

test/mailers/previews/user_mailer_preview.rb

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  ・
  ・
  ・

  # 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
end

パスワード再設定用メイラーメソッドのテストを追加する。

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


12.3 パスワードを再設定する