こんにちは!kossyです!
今回は、ログイン周りの追跡を実現する、deviseの「trackable」のソースコードを追ってみたので、ブログに残してみたいと思います。
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を参照してください。
この変更のPRはこちらですね。
余談ですが、「どうやってテストすればいいかわからない」というコメントに対して、「統合テストを作成し、検証が実行された場合にクラスにグローバル値を設定する検証をモデルに追加すればいいよ」とアドバイスをしているのが大変参考になります。。。
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のコードリーディングの入り口に向いているのではないかと思います。