Module: Sessions::DeviceDisplay

Extended by:
ActiveSupport::Concern
Included in:
Event, Model
Defined in:
lib/sessions/models/concerns/device_display.rb

Overview

Human, honest device presentation — shared by the registry rows (Sessions::Model) and the trail (Sessions::Event), which carry the same parsed device columns:

"Chrome 137 on macOS"
"MyApp 2.4.1 on Pixel 8 (Android 16)"
"iPhone (iOS 19.5)"

Frozen-UA tokens are never rendered as facts — versions appear only where they’re real (see Sessions::Device).

Instance Method Summary collapse

Instance Method Details

#auth_method_labelObject

“Google”, “GitHub”, “password”, “passkey”… for “Signed in via %method” copy. nil when the method is unknown (the UI omits the clause).



104
105
106
107
108
109
110
# File 'lib/sessions/models/concerns/device_display.rb', line 104

def auth_method_label
  method = try(:auth_method)
  return nil if method.blank? || method == "unknown"
  return try(:auth_provider).to_s.titleize if via_oauth? && try(:auth_provider).present?

  I18n.t("sessions.auth_methods.#{method}", default: method.humanize.downcase)
end

#bot?Boolean

Returns:

  • (Boolean)


72
73
74
# File 'lib/sessions/models/concerns/device_display.rb', line 72

def bot?
  try(:device_type) == "bot"
end

#country_flagObject

“🇪🇸” — derived from country_code at render time; no column needed.



38
39
40
41
42
43
# File 'lib/sessions/models/concerns/device_display.rb', line 38

def country_flag
  code = try(:country_code).to_s
  return nil unless code.match?(/\A[A-Za-z]{2}\z/)

  code.upcase.each_codepoint.map { |codepoint| (codepoint + 0x1F1A5).chr(Encoding::UTF_8) }.join
end

#device_nameObject



17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/sessions/models/concerns/device_display.rb', line 17

def device_name
  return sessions_t("bot", default: "Bot (%{name})", name: try(:browser_name) || "unknown") if bot?
  return sessions_native_device_name if hotwire_native?

  client = [try(:browser_name), try(:browser_version)].compact.join(" ").presence
  platform = sessions_os_label

  if client && platform
    sessions_t("composite", default: "%{client} on %{platform}", client: client, platform: platform)
  else
    client || platform || sessions_t("unknown", default: "Unknown device")
  end
end

#hotwire_native?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/sessions/models/concerns/device_display.rb', line 60

def hotwire_native?
  try(:device_type).to_s.start_with?("native_")
end

#locationObject

“Madrid, Spain” — or nil when geolocation is unavailable (the UI omits location cleanly).



33
34
35
# File 'lib/sessions/models/concerns/device_display.rb', line 33

def location
  [try(:city), try(:country_name)].compact_blank.join(", ").presence
end

#native_android?Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/sessions/models/concerns/device_display.rb', line 68

def native_android?
  try(:device_type) == "native_android"
end

#native_ios?Boolean

Returns:

  • (Boolean)


64
65
66
# File 'lib/sessions/models/concerns/device_display.rb', line 64

def native_ios?
  try(:device_type) == "native_ios"
end

#second_factorObject

The second factor that protected this login, when one did: “totp” (authenticator apps via devise-two-factor — detected automatically), “backup_code”, or whatever the host tagged (“webauthn” for security keys / Touch ID as a second factor — see the README’s two-factor recipes). nil for single-factor logins.



93
94
95
96
# File 'lib/sessions/models/concerns/device_display.rb', line 93

def second_factor
  detail = try(:auth_detail)
  detail.is_a?(Hash) ? detail["second_factor"].presence : nil
end

#second_factor?Boolean

Returns:

  • (Boolean)


98
99
100
# File 'lib/sessions/models/concerns/device_display.rb', line 98

def second_factor?
  second_factor.present?
end

#source_line(ip: true, separator: " · ") ⇒ Object

“🇪🇸 Madrid, Spain · IP 83.45.112.7 · Firefox 139 on Windows” — the one-line WHERE-then-WHAT of a login, ready for security emails, notification bodies, and admin lists. Location leads (people recognize places; browser version numbers mean nothing to them), the IP is the verifiable fact, the device closes. Each part drops out cleanly when the record lacks it (no geo in dev, no UA on odd clients). ‘ip: false` for compact UI like notification feed rows; `separator:` for plain-text contexts.



53
54
55
56
57
58
# File 'lib/sessions/models/concerns/device_display.rb', line 53

def source_line(ip: true, separator: " · ")
  located = [country_flag, location].compact.join(" ").presence
  address = ip ? (try(:ip_address) || try(:last_seen_ip)).presence : nil

  [located, address && "IP #{address}", device_name].compact.join(separator).presence
end

#via_oauth?Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/sessions/models/concerns/device_display.rb', line 80

def via_oauth?
  try(:auth_method) == "oauth"
end

#via_password?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'lib/sessions/models/concerns/device_display.rb', line 84

def via_password?
  try(:auth_method) == "password"
end

#web?Boolean

Returns:

  • (Boolean)


76
77
78
# File 'lib/sessions/models/concerns/device_display.rb', line 76

def web?
  !hotwire_native? && !bot?
end