基礎編21

2021.4.5

パスワードリセット機能の実装

 

sorceryのreset_passwordモジュールを使用する。

(イメージ)

ログイン画面のパスワードおお忘れの方(リンク)からパスワード変更の申請ページへ

パスワード変更申請ページからメールアドレスを入力→

メールアドレスにメールが届く→メールのURLを開くとパスワード申請ページが表示され、パスワードを変更できる。

 

参照

【Rails】パスワード変更(トークンがどのように使用されているのか) - Qiita

sorceryのパスワードリセットを実装(reset_passwordモジュール) - Qiita

Action Mailer の基礎 - Railsガイド

 

<エラー>

$ rails db:mograte:resetを実行したいときに、まず現状を確認したく

$rails db:migrate:statusを実行したところ、

Schema migrations table does not exist yet. (=スキーママイグレーション・テーブルがまだ存在しません。)

と出てしまった。

rails db:migrate を実行すれば存在するのか?と実行したところ、

 Multiple migrations have the name SorceryResetPassword.(=複数のマイグレーションにSorceryResetPasswordという名前がついています。)と教えてくれた。

原因:内容が全く同じmigeationファイルが2つあった。1つを削除したところ、実行できた。

参考

Ruby on Rails - rake db:migrateが実行されない|teratail

 

❷ 

$bundle exec rspecを実行すると

Capybara::ElementNotFound:

Unable to find field "メールアドレス" that is not disabledが表示される。

(原因)inputとlabelが紐づいていなくて、具体的にはlabelのfor属性とinputのnameやidなどが一致してない時に出る。

form_withを正しく使えていればRailsがうまいこと紐づけてくれるはずなのでform_withの使い方が何かおかしい...

(元々)

<div class="form-group">
<%= f.label :メールアドレス %>
<%=f.text_field :email, class:"form-control" %>
</div>

f:id:mmm_st:20210406101210p:plain

(変更後)

f:id:mmm_st:20210406101239p:plain

 

このように変更したが...エラーに変更なし!

よく考えてみると、「メールアドレス」と表示されていない。ので指定。

<div class="form-group">
<%= f.label :email, t(User.human_attribute_name(:email)) %><br />
<%= f.email_field :email, class: 'form-control' %>
</div>

 

f:id:mmm_st:20210406101922p:plain

 

これでOK

 

参照

<LABEL>-HTMLタグリファレンス

 

<流れ>

sorceryのreset_passwordモジュールを使用する

参照

Reset password · Sorcery/sorcery Wiki · GitHub

--導入する

$ rails g sorcery:install reset_password --only-submodules

--migrationファイルが作成される

class SorceryResetPassword < ActiveRecord::Migration[5.2]
def change
add_column :users, :reset_password_token, :string, default: nil
add_column :users, :reset_password_token_expires_at, :datetime, default: nil
add_column :users, :reset_password_email_sent_at, :datetime, default: nil
add_column :users, :access_count_to_reset_password_page, :integer, default: 0
 
add_index :users, :reset_password_token
end
end

 $ rails db:mograte

 

tokenにユニーク制約とallow_nil: trueを追加

--パスワードを変更した際に、reset_password_tokennilになるのでユニーク制約にひっかる。ここでallow_nil: trueを追加せることでnilを許可する。

allow_nil: true: 対象の値がnilの時にバリテーションをスキップする

(user.rb)

validates :reset_password_token, uniqueness: true, allow_nil: true

 

UserMailerという名前のパスワードリセットメール用のMailerを作成

--パスワードリセットに使用するメイラーUserMailerを作成

$ rails g mailer User.Mailer reset_password_email

 

--sorceryのパスワードリセットに使用するActionMailerとして、UserMailerを指定する。

ActionMailerRuby on Rails に組み込まれているメール送信機能

(1)からapp/mailers/user_mailer.rbが推測される

(config/initializers/sorcery.rb)

Rails.application.config.sorcery.submodules = [:reset_password]
 
Rails.application.config.sorcery.configure do |config|
config.user_config do |user|
user.reset_password_mailer = UserMailer (1)
end
 
end

 

--パスワードリセット用のメソッドを記述

・パスワードリセット用メールを送信するためのメソッドを作成し(1)、引数にuserパラメータを追加する

・メールに表示させる情報やメールの送信先を設定(2)

・メールのviewで使用する(3)

(app/mailers/user_mailer.rb)

class UserMailer < ApplicationMailer
def reset_password_email(user) (1)
@user = User.find(user.id) (3)
@url = edit_password_reset_url(@user.reset_password_token) (3)
mail(to: user.email, subject: t('defaults.password_reset')) (2)
end
end

Mailerクラス#メソッドはController#アクションと似た動きをする

今回は)

user_mailerクラス#reset_password_emailメソッド という形で

user_mailer/reset_password_email.html.erb(text.html.erbも)というメイラーviewを推測し呼び出す。

 

--送信元のメールアドレスと宛名を指定する

(app/mailers/application_mailer.rb)

class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

 

--メイラーviewを設定する

上で定義したインスタンス変数(@user.@url)を使用している

(app/views/user_mailer/reset_password_email.html.erb)

<p><%= @user.decorate.full_name %></p>
<p>===============================================</p>
 
<p>パスワード再発行のご依頼を受け付けました。</p>
 
<p>こちらのリンクからパスワードの再発行を行ってください。</p>
<p><a href="<%= @url %>"><%= @url %></a></p>

app/views/user_mailer/reset_password_email.text.erb)

<%= @user.decorate.full_name %>
===============================================
 
パスワード再発行のご依頼を受け付けました。
 
こちらのリンクからパスワードの再発行を行ってください。
<%= @url %>

 

routes.rbに 記述

(routes.rb)

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

 

パスワードアクションを処理するコントローラーを作成する

--controller作成

$ rails g controller PasswordReset create edit update new

 

--controller内容解説

 

(1)newアクション: 

パスワードリセット申請用フォーム用のアクション

 

 

(2) createアクション: 

パスワードリセットをリクエスする。ユーザーがパスワードのリセットフォームにemailを入力し、送信した時にこのアクションが実行される。(=パスワードリセット対象のメールアドレスを申請し、リセット対象を作成する)

❶(views/passeord_resets/new.html.erb)のform_withで送られてきたemailをparamsで受け取る

❷DBにデータがあれば、メールをユーザに送信する(URL付き)

(@user.deliver_reset_password_instructions! if @user と同じ)

❸ 入力したemailがDBに存在するか否かを問わず、リダイレクトし成功メッセージを表示させる。悪意ある第三者にemailがDBにあるかどうかを確認させないため。

 

 

(3)editアクション:

ユーザーがメールのURLをクリック→パスワードリセットフォームページ(views/passeord_resets/edit.html.erb)へ→新しいパスワードを入力。

❶ メールからpostされてきた値を取得

❷ リクエストで送信されてきたトークンを使用し、ユーザの検索、有効期限のチェックを行う。

トークンが見つかり、有効であればそのユーザオブジェクトを@userに格納する。

❸ @userがnilまたは空の場合、not_authenticatedメソッドを実行する。

not_authenticatedメソッド: applicatio_controller.rbに入力してある

 

 

(4) updateアクション:

(views/passeord_resets/edit.html.erb)でユーザーが「更新する」ボタンでパスワードを変更した時に動く。

❶❷❸はeditと同じ感じ

❹パスワード確認の検証を行う。

@user.password_confirmationにparams[:user][:password_confirmation]を代入している...よね?

❺ change_passwordメソッド: パスワードリセットに使用したトークンを削除し、パスワードを更新する。

 

(app/controllers/password_resets_controller.rb)

class PasswordResetsController < ApplicationController
skip_before_action :require_login

def new; end (1)

def create (2パスワードリセットをリクエストする)
@user = User.find_by(email: params[:email]) ❶
@user&.deliver_reset_password_instructions! ❷
redirect_to login_path, success: t('.submit_message') ❸
end

def edit (3 パスワードリセットフォームページへ遷移する)
@token = params[:id] ❶
@user = User.load_from_reset_password_token(params[:id]) ❷
return not_authenticated if @user.blank? ❸
end

def update (4 ユーザーが新パスワードを入力した時に実行される)
@token = params[:id] ❶
@user = User.load_from_reset_password_token(@token) ❷
return not_authenticated if @user.blank? ❸

@user.password_confirmation = params[:user][:password_confirmation] ❹
if @user.change_password(params[:user][:password]) ❺
redirect_to login_path, success: t('.success')
else
flash.now[:danger] = t('.fail')
render :edit
end
end
end

 

パスワードリセット用のフォーム作成

--パスワードリセット申請フォーム

(views/passeord_resets/new.html.erb)

<% content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
<div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t('.title') %></h1>
<%= form_with url: password_resets_path, local: true do |f| %>
<div class="form-group">
<%= f.label :email, t(User.human_attribute_name(:email)) %><br />
<%= f.email_field :email, class: 'form-control' %>
</div>
<%= f.submit t('defaults.submit'), class: 'btn btn-primary' %>
<% end %>
</div>
</div>
</div>

--パスワードリセット用フォーム

(views/passeord_resets/edit.html.erb)

<% content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
<div class="col col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t('.title') %></h1>
<%= form_with model: @user, url: password_reset_path(@token), local: true do |f| %>
<%= render 'shared/error_messages', object: f.object %>
 
<div class="form-group">
<%= f.label :email %>
<%= @user.email %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<div class="actions">
<p class="text-center">
<%= f.submit class: 'btn btn-primary' %>
</p>
</div>
<% end %>
</div>
</div>
</div>

 

letter_opener_webを追加し、開発環境では実際のメールは送られないように設定する

--Gemfileに追加する

group :development do
省略
 
gem 'letter_opener_web', '~> 1.4.0'
省略
 
end

$ bundle install

 

--routes.rbにLetterOpenerWebにアクセスするために必要な記述を追記

(config/routes.rb)

mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?

 

--config/environments/development.rbに設定を追加

(config/environments/development.rb)

Rails.application.configure do
省略
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.default_url_options = Settings.default_url_options.to_h
end

 

--メールが送れたか確認する

http://localhost:3000/letter_opener

ここにアクセセスする。

 

host情報について

--configというgemを使用してsettings/development.ymlに記載する。

今回は開発環境での設定をする為、(settings/development.yml)に記載する。

テスト環境であれば、(settings/test.yml)に記載。configフォルダ以下に管理するとメンテナンスが楽になるから。環境ごとに値が変わらない場合は、(config/settings.yml)に記述するといい。

GitHub - rubyconfig/config: Easiest way to add multi-environment yaml settings to Rails, Sinatra, Pandrino and other Ruby projects.

host : サービスする役割のコンピュータのこと

 

(Gemfile)

gem 'config'

$ bundle install

カスタマイズ可能な構成ファイル(settings/development.yml)とデフォルト設定ファイルが生成される。

 

--(settings/development.yml)の設定を定数で置き換える

default_url_options:
host: 'localhost:3000'

 

(config/environments/development.rb)に

config.action_mailer.default_url_options = { host: 'localhost:3000' }

が入る。