大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Devise】Recoverable

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回も一つ前の投稿と同じようにdeviseのモデル設定についての記事となります。

今回はRecoverableについてざっくりとgemの中身を見ながら理解していきます。

概要

モデルやモデルに対応するテーブルでのdeviseの記述は以下のようになります。

  • モデル
# app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
class AddDeviseToUsers < ActiveRecord::Migration[5.2]
  def self.up
    change_table :users do |t|
・
・
      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
・
・

デフォルトの設定で有効になっていますね。ですので機能説明したデータベース認証を行うdatabase_authenticatable同様に重要な役割を担っていそうです。

マイグレーションファイルをよく見てみましょう。実はRecoverableの機能を推測できます。

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

reset_passwordとありますね。

そうです。まさにパスワードをリセットする機能を持っています。

ここでモジュールの中身を見ていきましょう。 Recoverableも重要な機能のせいかモジュールの中身がとても長いです。

github.com

一番上にある説明ですが、こちらは上述したことと同様の内容を説明しています。

Recoverable takes care of resetting the user password and send reset instructions

訳: Recoverableはユーザーパスワードの再設定を行い、再設定手順を送信します。

概要については以上です。

3つのオプション

概要については理解できたかと思いますが、モジュールの以下の記述はどうでしょう。

    # ==Options
    #
    # Recoverable adds the following options to devise_for:
    #
    #   * +reset_password_keys+: the keys you want to use when recovering the password for an account
    #   * +reset_password_within+: the time period within which the password must be reset or the token expires.
    #   * +sign_in_after_reset_password+: whether or not to sign in the user automatically after a password reset.

オプションとあるように追加で設定を加えることができるようですね。 そして、オプションの数は以下の3つです。

  • reset_password_keys(パスワードをリセットする際に使用するキー)
  • reset_password_within(パスワードのリセットの有効期限や、リセットトークンが失効する期限を設定)
  • sign_in_after_reset_password(パスワードリセット後に自動的にサインインするかどうか)

少し理解がしづらいのはreset_password_keysの説明かと思います。

こちらはパスワードをリセットする際に使用するモデルのカラムを指定します。 例えば、リセット時に案内のメールを送信するときに、メールアドレスが必要ですね。 そんなときにキーを指定してあげることでモデルのメールアドレスに

オプションの設定方法としてはdevise_for:とあることから、モデルで設定ができるようですが、多くの記事を見るとinitializerは以下に作成されるdevise.rbに記述するようです。

# devise.rb

# パスワードをリセットする際に使用するキーをemailに設定
config.reset_password_keys = [:email]

# リセットの有効期限や、リセットトークンが失効する期限を10時間に設定
config.reset_password_within = 10.hours

# パスワードリセット後に自動的にサインインすることを許可
config.sign_in_after_reset_password = true

モジュール内

モジュール内で定義されているパスワードのリセットに関するメソッドなどを見ていきましょう。

モジュールの記述の概要を説明している箇所でこのモジュールの機能のExamplesの項目があります。

# == Examples
    #
    #   # resets the user password and save the record, true if valid passwords are given, otherwise false
    #   User.find(1).reset_password('password123', 'password123')
    #
    #   # creates a new token and send it with instructions about how to reset the password
    #   User.find(1).
  • reset_password

1つ目の例はこのモジュールの根幹となるreset_passwordメソッドです。

      # Update password saving the record and clearing token. Returns true if
      # the passwords are valid and the record was saved, false otherwise.
      def reset_password(new_password, new_password_confirmation)
        if new_password.present?
          self.password = new_password
          self.password_confirmation = new_password_confirmation
          save
        else
          errors.add(:password, :blank)
          false
        end
      end

役割が英語で説明されていますね。

Update password saving the record and clearing token. Returns true if the passwords are valid and the record was saved, false otherwise.

訳: パスワードを更新しレコードを保存、トークンをクリアします。パスワードが有効でレコードが無事に保存された場合はtrueを、そうでない場合はfalseを返します。

第一引数にnew_password、第二引数にnew_password_confirmationとあります。これはパスワードを別のパスワードに更新する際に、一度の入力だけでなく確認のためにもう一度入力するという仕様を明示しています。例でも同じパスワードを入力しています。

User.find(1).reset_password('password123', 'password123')

[パスワードリセット画面の例]

https://i.gyazo.com/9673a68fa9dd3f649f93d8571b093858.png

new_passwordが入力されているのであれば、モデルのpasswordカラムとpassword_confirmationカラムに、それぞれnew_passwordnew_password_confirmationを代入し、saveメソッドで保存しています。

if new_password.present?
  self.password = new_password
  self.password_confirmation = new_password_confirmation
  save
  • send_reset_password_instructions

2つ目はsend_reset_password_instructionsメソッドです。

これもメソッド名から何をしているのかがすぐにわかります。パスワードをリセットする際に自分宛てにメールが送られリセットのフローを説明してくれる機能ですね。

※ 本当に公式のモジュールのメソッドは引数やメソッド名から機能を予測できるようにうまく記載されていて勉強になります!

https://i.gyazo.com/0c136a8a4a15f7e99f9b75f2012065dd.png

定義されている箇所は以下になります。

      # Resets reset password token and send reset password instructions by email.
      # Returns the token sent in the e-mail.
      def send_reset_password_instructions
        token = set_reset_password_token
        send_reset_password_instructions_notification(token)

        token
      end

Resets reset password token and send reset password instructions by email.

訳: リセットパスワードのトークンと指示メールをリセットします。

まず、set_reset_password_tokenメソッドを使用して新しいリセットパスワードのトークンを生成しています。

def set_reset_password_token
  raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)

  self.reset_password_token   = enc
  self.reset_password_sent_at = Time.now.utc
  save(validate: false)
  raw
end

Devise.token_generatorとあるようにトークンを生成してくれる機能があるようです。そしてreset_password_tokenカラムに生成したトークンを代入しreset_password_sent_atカラムには現在時刻を協定世界時 (UTC)で代入します。

Time.now.utc
=> 2021-03-27 00:06:00 UTC

そして、send_reset_password_instructions_notificationメソッドによってメールを送信していると思われます。下記が定義されている箇所です。

def send_reset_password_instructions_notification(token)
  send_devise_notification(:reset_password_instructions, token, {})
end

send_devise_notificationメソッドに関しては別のモジュールであるAuthenticatableで定義されているようです。

def send_devise_notification(notification, *args)
  message = devise_mailer.send(notification, self, *args)
  # Remove once we move to Rails 4.2+ only.
  if message.respond_to?(:deliver_now)
    message.deliver_now
  else
    message.deliver
  end
end

message.deliver_nowとあるようまさにメールを送信している箇所ですね。*argsの中にtokenが入っているのでtokenもメールに添付されて送られるのだと思います。これによりパスワードをリセットされるメールがユーザーに届くというわけですね!