パスワードリセット

ActionMailerを使う

letter_openerというgemを使う

トークンにユニーク制約をする

ログイン画面にリセットページのリンク作成

開発環境では送れないようにする

リセット画面の作成

クリックすると反応するメールのフォームとラベルをつける

リセットの申請をするとログインページに戻る

OKでもNGでも送信完了のフラッシュが出る

Sorcery/sorcery

githubに手順が書いてある

もしくはこれ

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

mailerはこれ

Action Mailer の基礎 - Railsガイド

・migrationのエラー


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

sorceryの機能を使うため上を入力。

Addでマイグレーションを自動追加になるがdb:migrate出来ない現象に陥った。理由はテーブル名の違い。 sorceyの機能で作成したカラムではUserと出ていたが、実際今まで作っていたのはusersというテーブル名だったのでAddのマイグレーションファイル内のテーブル名をUserからusersに変える必要があった。

上記のsorceryのコマンドで生成されたsorcery.rbの中に[:reset_password]というのが7行目あたりに記載されているはずだ、もしなければ[]内に記載する。

また、その下にあるRails.application.config.sorcery.configure do |config| ももし無ければ記載する

# The first thing you need to configure is which modules you need in your app.
# The default is nothing which will include only core features (password encryption, login/logout).
#
# Available submodules are: :user_activation, :http_basic_auth, :remember_me,
# :reset_password, :session_timeout, :brute_force_protection, :activity_logging,
# :magic_login, :external
Rails.application.config.sorcery.submodules = [:reset_password]  # ← ここ

# Here you can configure each submodule's features.
Rails.application.config.sorcery.configure do |config|
  # -- core --
  # What controller action to call for non-authenticated users. You can also

ここでの注意は下にコメントアウトされている中で既に記載があるものは重複して書かないようにすること。必要なものはコメントアウトを外して利用する。

なので今回必要なのは

244行目あたりの config.user_config do |user|

394行目あたりの # user.reset_password_mailer = の部分にUserMailerを付け足す

395行目あたりに end

554と555行目あたりのconfig.user_class = 'User' と end である

まとめると以下の表示だが、qiitaやgithubにはそこまで書かれていないため コメントアウトインデントに気をつけて利用する

Rails.application.config.sorcery.submodules = [:reset_password]

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.reset_password_mailer = UserMailer
  end
	config.user_class = 'User'
end

・mailerの作成


$ rails g mailer UserMailer reset_password_email

上記をすることでapp/mailersファイルが生成される

viewsの中にもtextとhtml用のファイルがあり、中には届くメール内容を記述する

app/views/user_mailer/reset_password_email.text

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

app/views/user_mailer/reset_password_email.html.erb

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

<p><a href="<%= @url %>"><%= @url %></a></p>

上記の@urlにはtokenが発行された再登録用のurlが入る

app/mailers/user_mailer.rb

class UserMailer < ApplicationMailer
	def reset_password_email(user)
	    @user = User.find(user.id)
	    @url = edit_password_reset_url(@user.reset_password_token)
	    mail(to: user.email,
	         subject: 'パスワードリセット')
  end
end

・コントローラーの作成


$ bundle exec rails g controller PasswordResets new create edit update

中身を書いていく

class PasswordResetsController < ApplicationController
  skip_before_action :require_login
  # ログインする前にパスワード変更のページで使いたいので上記が必要
  def new; end

  def create #パスワードリセット対象のメールアドレスを申請して、リセットの対象を作成する
    @user = User.find_by(email: params[:email]) #メールアドレスの登録があれば@userに渡す
    @user&.deliver_reset_password_instructions!
    redirect_to login_path, success: t('.success')
  end

  def edit #パスワードリセット用のメールのURLをクリックして、新しいパスワードを入力する。
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)
    return not_authenticated if @user.blank?
  end

  def update #パスワードリセット(edit)ページにて、『更新する』ボタンでパスワードを変更する。
    @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

コントローラー内で出てきたメソッドの意味(github内で検索)


**deliver_reset_password_instructions** メソッド

# Generates a reset code with expiration and sends an email to the user.
	# 期限付きのリセットコードを生成し、ユーザーにメールを送信します。
          def deliver_reset_password_instructions!
            mail = false
            config = sorcery_config
            # hammering protection # ハンマーによる保護
            return false if config.reset_password_time_between_emails.present? && send(config.reset_password_email_sent_at_attribute_name) && send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.seconds.ago.utc

**load_from_reset_password_token** メソッド

module ClassMethods
          # Find user by token, also checks for expiration.
						# トークンでユーザーを見つけ、有効期限もチェックします。
          # Returns the user if token found and is valid.
						# トークンが見つかり、有効であればユーザーを返します。
          def load_from_reset_password_token(token, &block)
            load_from_token(
              token,
              @sorcery_config.reset_password_token_attribute_name,
              @sorcery_config.reset_password_token_expires_at_attribute_name,
              &block
            )
          end

**change_password** メソッド

# Clears token and tries to update the new password for the user.
	# トークンをクリアして、ユーザーの新しいパスワードの更新を試みます。
          def change_password(new_password, raise_on_failure: false)
            clear_reset_password_token
            send(:"#{sorcery_config.password_attribute_name}=", new_password)
            sorcery_adapter.save raise_on_failure: raise_on_failure
          end

・メール送信の確認、環境変数の設定


Gemfileにletter_openerとconfigを追加する

  gem 'config' #このgemが環境変数を変更するのでグループの指定はせず外に書く

group :development, :test do  #開発環境とテスト環境で使えるようにするため,このgroupに記入
	gem 'letter_opener_web', '~> 1.0'
end
 $ bundle install

config

ホスト情報は開発環境、ステージング環境、本番環境で異なる


configのgemを入れると環境にまつわるlocalやdevelopmentなど…のファイル生成される。

さまざまな環境に柔軟に対応するために今回は config/settings/development.yml に

 host = Setting.host 

とするとすっきりするだけでなく、管理する箇所が環境のファイル1箇所にまとまるので保守性も非常に高まる。

letter_opener

開発環境などで送信したメールを確認するため


・ルーティングの編集


コントローラーを追加したので遷移先を指定する。

それと同時にメールが届いたかを判断するためにletter_openerを使用するために以下も追加する

resources :password_resets, only: %i[new create edit update]
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
	# ↑ 'localhost:3000/letter_opener'で開くことができる

Userモデル

allow_nil: trueを追加

パスワードを変更した際、reset_password_tokenがnilになるのでユニーク制約に引っかかってしまう。そこで、allow_nil:trueを加えることでnilを許可しておく。

validates :reset_password_token, uniqueness: true, allow_nil: true

config/environments/test.rb

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

config/environments/development.rb

config.action_mailer.perform_caching = false # 36行目あたりコメントアウト

config.action_mailer.default_url_options = { host: Settings.host }
 # 一番下部分をコメントアウトし {}内を編集

config.action_mailer.delivery_method = :letter_opener_web # ← 追加

・ビュー


ここまで出来たらコントローラー作成時にできたviewファイルに、送信フォームのhtmlを書いていく

:password変更のメールを送信するページ app/views/password_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('password_resets.new.submit'), class: 'btn btn-primary' %>
     <% end %>
     </div>
   </div>
 </div>

:メール送信後にpasswordを再設定するページ app/views/password_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>

:ログイン画面下にリンクを表示させる app/views/user_sessions/new.html.erb

<% end %>
       <div class='text-center'>
         <%= link_to (t '.to_register_page'), new_user_path %>
         <%= link_to (t '.password_forget'), new_password_reset_path %>
       </div>                     # ↑ この部分
     </div>
   </div>