LoginSignup
0
0

More than 5 years have passed since last update.

railsチュートリアル 第十二章

Last updated at Posted at 2019-03-26

はじめに

railsチュートリアル第十二章を進めていく中で、理解しにくかったところなど復習用に要約しているものとなっています!

パスワードの再設定

第十一章でアカウントの有効化が済み、十二章ではパスワードの再設定を実装していく。

また、実装していく流れは十一章の有効化と似通っている部分が多くある。

パスワード再設定の全体の流れは次の通り。

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

今回も有効化の時と同様に、新たなモデルは作らずに、代わりに必要なデータ (再設定用のダイジェストなど) を Userモデル に追加していく形で進めていく。

PasswordResetsコントローラ

最初のステップとして、パスワード再設定用のコントローラを作っていく。

今回はビューも扱うので有効化と違い、newアクションeditアクション も一緒に生成しておく。

$ rails generate controller PasswordResets new edit 

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

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
  #追加
  resources :password_resets,     only: [:new, :create, :edit, :update]
end

上記のコードは以下のRESTfulのルーティングに従い、名前付きルートを使っている。

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)

パスワード再設定画面へのリンクを追加しておく。

app/views/sessions/new.html.erb
.
.
.
      <%= f.label :password %>
      #追加
      <%= link_to "(forgot password)", new_password_reset_path %>
      <%= f.password_field :password, class: 'form-control' %>
.
.
.

新しいパスワードの設定

アカウント有効化の場合のように、パスワードの再設定でも、トークン用の仮想的な属性とそれに対応するダイジェストを用意していく。

もしトークンをハッシュ化せずに (つまり平文で) データベースに保存してしまうとするとセキュリティ的にダメだから再設定でもダイジェストを使うようにする。

また、再設定用のリンクに期限を設けるため、メールの送信時刻を記録する属性も追加する必要がある。

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

↑のようにreset_digest:stringreset_sent_at:datetimeusersテーブルに追加した。

新しいパスワード再設定の画面はログインの画面を参考に以下のようなものにする。

app/views/sessions/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>

createアクションでパスワード再設定

フォームから送信を行なった後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要がある。

それに続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示する。

また送信が無効だった場合の処理も書いておく。

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
#省略
# パスワード再設定の属性を設定する
  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

この時点でのアプリケーションは、無効なメールアドレスを入力した場合に正常に動作し、エラーメッセージを表示する。

正しいメールアドレスを送信した場合にもアプリケーションが正常に動作するためには、パスワード再設定のメイラーメソッドを定義する必要がある。

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

PasswordResetsコントローラで、createアクションがほぼ動作するところまできたから、次にパスワード再設定に関するメールを送信する部分を進めていく。

ここでは第十一章で生成したUserメイラー (app/mailers/user_mailer.rb)にデフォルトの password_resetメソッド もまとめて生成されているからこちらの方で処理をかく。

パスワード再設定のメールとテンプレート

UserMailer.password_reset(self).deliver_now

上のコードを実装するためにUserメイラーpassword_resetメソッド を作成し、続いて、テキストメールのテンプレートとHTMLメールのテンプレートをそれぞれ定義していく。

app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  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>

editアクションで再設定

無事に送信メールを生成できたので、次はPasswordResetsコントローラeditアクション の実装を進めていく。

送られてきたurlをクリックして表示される、パスワード再設定のフォーム次のようなものとする。

app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages' %>
      #隠しフィールド
      <%= hidden_field_tag :email, @user.email %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

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

この場合はメールアドレスをキーとしてユーザーを検索するためには、editアクションとupdateアクションの両方でメールアドレスが必要になる。

ここで隠しフィールドとしてメールアドレスをページ内に保存することで、他の情報と一緒にメールアドレスが送信されるようになる。

editアクションupdateアクションのどちらの場合も正当な @user が存在する必要があるので、いくつかのbeforeフィルタを使って @user の検索とバリデーションを行う。

app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  before_action :get_user,   only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
  .
  .
  .
  def edit
  end

  private

    def get_user
      @user = User.find_by(email: params[:email])
    end

    # 正しいユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end
end

パスワードを更新する

AccountActivationsコントローラeditアクション では、ユーザーの有効化ステータスをfalseからtrueに変更したが、今回の場合はフォームから新しいパスワードを送信する事になるからフォームからの送信に対応する updateアクション が必要になる。

updateアクション では、以下の4つを考慮する必要がある。

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

これらを考慮して以下のようにを定義する。

app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  before_action :get_user,         only: [:edit, :update]
  before_action :valid_user,       only: [:edit, :update]
  #追加
  before_action :check_expiration, only: [:edit, :update]    # (1) への対応

#省略

  def update
    if params[:user][:password].empty?                  # (3) への対応
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update_attributes(user_params)          # (4) への対応
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'                                     # (2) への対応
    end
  end

  private

    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end
    .
    .
    .
    # トークンが期限切れかどうか確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
end

パスワード再設定の有効期限が切れていないか
に関しては、check_expirationメソッドを定義して解決する。

check_expirationメソッドを実装するには
password_reset_expired?
を定義する必要がある。

このメソッドではパスワード再設定の期限を設定して、2時間以上パスワードが再設定されなかった場合は期限切れとする処理を行う。

それではUserモデルで password_reset_expired?メソッド を定義する。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  private
    .
    .
    .
end

reset_sent_at < 2.hours.ago
は、
「パスワード再設定メールの送信時刻が、現在時刻より2時間以上前 (早い) の場合」

という風に解釈できる。

これでパスワード再設定の実装も終わり、あとは前章のようにproduction環境でも動くようにする。

セットアップの手順はアカウント有効化と全く同じとなっている。

さいごに

パスワード再設定はアカウント有効かと似通う部分が多い。

次↓
https://qiita.com/jonnyjonnyj1397/items/01a5dfeee3a4e9133266

0
0
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
0
0