[Day14 Rails Tutorial]第十二章完|パスワード再設定機能の実装[12.1〜12.6]

Day14です!

今日もがんばっていきましょー!

読んでいただいている方、いつもありがとうございます!

初心者なもので、たまに間違った内容もあるかと思います。

その際にはコメントやTwitterでご指摘いただけると幸いです!

 

[Day14]第十二章12.1〜12.6作業ログ

今日はパスワード再設定のメール機能の実装

登録しているサービスで、パスワードを忘れちゃうことってありますよね。

その時にパスワードを再設定するためのメールを受け取ったりします。

今日はこのパスワード再設定のメール機能の実装!

昨日やった、アカウント認証と似ている部分がけっこうあるみたいなので、サクサクすすみそう。

 

PasswordResetsコントローラとルーティングの設定

なにはともあれ、まずはコントローラーとルーティングですよ。

コントローラーの作成。

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

最後のオプションは、テストファイルを作成したくないときに使います。

ルーティングの追加

resources :password_resets, only: [:new, :create, :edit, :update]

これで下の表のような名前付きリンクが使えるようになります。

 

ログイン画面にパスワード再設定のページへのリンクを設置

さっきの名前付きルートを使ってあげましょう。

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 :password %>
      <%= link_to "(forgot password)", new_password_reset_path %>
      <%= f.password_field :password, class: 'form-control' %>
      ...
      <% end %>
      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>
    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

 

activation_digestにならって、reset_digestカラムやビューの追加

上の表のようなカラムにします。コマンドは次。

rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetimer?

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>

 

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

app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  ...
  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
  ...
end

太字のメソッドをこれから定義して、メールの送信やパスワードダイジェストの作成を行います。

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 activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end
 
  # 有効化用のメールを送信する
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end
 
  # パスワード再設定の属性を設定する
  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
 
    # メールアドレスをすべて小文字にする
    def downcase_email
      self.email = email.downcase
    end
 
    # 有効化トークンとダイジェストを作成および代入する
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

メーラーメソッドの設定がまだなので、メールはまだ送れません。

パスワード再設定用のメーラーメソッドはこれから定義していきます。

 

メーラーメソッドの設定

次にメーラーメソッドの設定をしてあげます。

メールの宛先、件名はきのうとほとんど同じ。

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

メールのテンプレートはこんな感じ。

リンクにメールアドレスとreset_tokenを含めてあげるところがポイント。

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ページの作成

これまでの実装でパスワード再設定のために必要な、リンクの設定ができました。

次はそのリンクからemailとreset_tokenを読み取って、パスワードの再設定を実際に行えるようにしていきます。

ここで、新パスワードの設定のためのeditアクションとパスワード更新のためのupdateアクションの2つのアクションで、リンクから読み取ったemailの値を使います。

そのために、一時的にemailの値を隠しフィールドとして保持できる手法を使います。

下のhidden_field_tagが隠しフィールドです。

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アクションに行く前に、有効なユーザーなのか検証できるようにしておきます。

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

 

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 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
 
  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
 
    # beforeフィルタ
 
    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
 
    # トークンが期限切れかどうか確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
end

password_reset_expired?メソッドはUserモデルに書いてあげます。

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

 

ってことで今日の実装は終了!

Herokuにデプロイして、動作確認もできました!

 

開発(develop)環境からメールが送信されずびっくり

ちょっと余談です。自分で勝手にパニックになってただけの話。

プレビューやテストではメールが送られたようになっているのに、実際にぼくのgmailにメールが届きませんでした。

そこでどこかで間違えたか、ソースコードを読み返しまくりました。

 

でも、よく考えてみれば実際に送られてこなくて当たり前だったんです。

昨日も開発環境からはメールは送信できなかったことを思い出しました。

メールが送信できたのは、本番(production)環境のHerokuからだけだった。

これはおそらく、開発環境ではメーラーのSMTPとかの設定をしていないからだと思う。

メーラーの設定してないからメールを送信できなくて当たりまえ!

 

[Day14]まとめ

  • 学習範囲:12.1〜12.6
  • 学習時間:3時間40分
  • 総学習時間:59時間
  • 反省点:
  • 備考:今週中に一周目終わらせたい。あと2日間で2章。できるはず。

読んでいただいている方、いつもありがとうございます!

初心者なもので、たまに間違った内容もあるかと思います。

その際にはコメントやTwitterでご指摘いただけると幸いです!

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です