ログイン周りの情報の追跡を実現する、deviseの「trackable」のソースコードを追ってみる

こんにちは!kossyです!



今回は、ログイン周りの追跡を実現する、deviseの「trackable」のソースコードを追ってみたので、ブログに残してみたいと思います。




環境

Ruby 2.6系
Rails 6.0.4
devise 4.8.0




github.com




trackableモジュールとは

ソースコード内のコメントアウト部分を訳してみます。

Track information about your user sign in. It tracks the following columns:

sign_in_count - Increased every time a sign in is made (by form, openid, oauth)
current_sign_in_at - A timestamp updated when the user signs in
last_sign_in_at - Holds the timestamp of the previous sign in
current_sign_in_ip - The remote ip updated when the user sign in
last_sign_in_ip - Holds the remote ip of the previous sign in


ユーザーのサインインに関する情報を追跡します。次の列を追跡します。

sign_in_count: サインインが行われるたびに増加します(form、openid、oauthによる)
current_sign_in_at: ユーザーがサインインしたときに更新されるタイムスタンプ
last_sign_in_at: 前のサインインのタイムスタンプを保持します
current_sign_in_ip: ユーザーがサインインするとリモートIPが更新されます
last_sign_in_ip: 前のサインインのリモートIPを保持します

出典: https://github.com/heartcombo/devise/blob/master/lib/devise/models/trackable.rb

trackableモジュールを導入することで何ができるようになるのかが一通りわかりました。

次の項で詳しくコードの中身を追ってみます。

required_fields

def self.required_fields(klass)
  [:current_sign_in_at, :current_sign_in_ip, :last_sign_in_at, :last_sign_in_ip, :sign_in_count]
end

trackableモジュールをincludeしているモデルに、配列内のsymbolと同名のメソッドが定義されているかを検証するために使うメソッドかと思われます。

このメソッドの参照箇所はこちらです。

devise/models.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub

def self.check_fields!(klass)
  failed_attributes = []
  instance = klass.new

  klass.devise_modules.each do |mod|
    constant = const_get(mod.to_s.classify)

    constant.required_fields(klass).each do |field|
      failed_attributes << field unless instance.respond_to?(field)
    end
  end

  if failed_attributes.any?
    fail Devise::Models::MissingAttribute.new(failed_attributes)
  end
end

instance.respond_to?(field)でメソッドの定義確認を行なっていました。

update_tracked_fields

def update_tracked_fields(request)
  old_current, new_current = self.current_sign_in_at, Time.now.utc
  self.last_sign_in_at     = old_current || new_current
  self.current_sign_in_at  = new_current

  old_current, new_current = self.current_sign_in_ip, extract_ip_from(request)
  self.last_sign_in_ip     = old_current || new_current
  self.current_sign_in_ip  = new_current

  self.sign_in_count ||= 0
  self.sign_in_count += 1
end

current_sign_in_atの値をold_currentとし、現在時刻をnew_currentとし、old_currentがあればそちらをlast_sign_in_atの値として採用して、なければnew_currentの値を採用しています。

そして、current_sign_in_atの値にnew_currentを代入しています。

extract_ip_fromメソッドは覗いてみる必要がありそう。

devise/trackable.rb at master · heartcombo/devise · GitHub

def extract_ip_from(request)
  request.remote_ip
end

requestオブジェクトのリモートIPを返すメソッドでした。

remote_ipメソッドの定義元はこちら(本ブログでの説明範囲を超えているのでソースコードだけ明記します)

rails/request.rb at 6-1-stable · rails/rails · GitHub

last_sign_in_ipの処理はlast_sign_in_atと似通ったものになっています。

残りの2行は、sign_in_countがnilの場合、0を代入しています。

最後にsign_in_countの値を1増加させています。


update_tracked_fields!

devise/trackable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub

def update_tracked_fields!(request)
  # We have to check if the user is already persisted before running
  # `save` here because invalid users can be saved if we don't.
  # See https://github.com/heartcombo/devise/issues/4673 for more details.
  return if new_record?

  update_tracked_fields(request)
  save(validate: false)
end

コメントアウト部分を訳してみます。

We have to check if the user is already persisted before running
`save` here because invalid users can be saved if we don't.
See https://github.com/heartcombo/devise/issues/4673 for more details.

実行する前に、ユーザーがすでに永続化されているかどうかを確認する必要があります
ここで save するのは、無効なユーザーを保存できるからです。
詳細については、https://github.com/heartcombo/devise/issues/4673を参照してください。

出典: https://github.com/heartcombo/devise/blob/c82e4cf47b02002b2fd7ca31d441cf1043fc634c/lib/devise/models/trackable.rb#L33

この変更のPRはこちらですね。

github.com

余談ですが、「どうやってテストすればいいかわからない」というコメントに対して、「統合テストを作成し、検証が実行された場合にクラスにグローバル値を設定する検証をモデルに追加すればいいよ」とアドバイスをしているのが大変参考になります。。。

update_tracked_fields!はどこから呼ばれているんでしょう。

devise/trackable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub

# frozen_string_literal: true

# After each sign in, update sign in time, sign in count and sign in IP.
# This is only triggered when the user is explicitly set (with set_user)
# and on authentication. Retrieving the user from session (:fetch) does
# not trigger it.
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
  if record.respond_to?(:update_tracked_fields!) && warden.authenticated?(options[:scope]) && !warden.request.env['devise.skip_trackable']
    record.update_tracked_fields!(warden.request)
  end
end

ここでした。ログイン後のhookで呼ばれているみたいです。


まとめ

カラムを用意してtrackableモジュールをincludeするだけで機能を追加できるので、とても便利かと思います。

また、deviseの-ble系のモジュールの中でも、内部のコードがかなり少ないモジュールでした。

加えて、update_tracked_fieldsとupdate_tracked_fields!のように、「値の代入」と「値の保存」を分けるようにコーディングしているのも、設計として面白いなと思いました。

コード量的にもOSSのコードリーディングの入り口に向いているのではないかと思います。