Module: Sessions::HasSessions

Extended by:
ActiveSupport::Concern
Defined in:
lib/sessions/models/concerns/has_sessions.rb

Overview

What ‘has_sessions` includes into the auth model. On Rails 8 omakase apps the generated `has_many :sessions, dependent: :destroy` already exists and is left alone; on Devise apps the association is declared here. Either way the model gains the events trail and the revocation verbs:

current_user.sessions.active
current_user.session_events.failed_logins.last_24_hours
current_user.revoke_other_sessions!     # "sign out everywhere else"
current_user.revoke_all_sessions!       # the account-takeover hammer

Plus the ASVS 3.3.3 default: changing the password revokes every other session (‘config.revoke_on_password_change`), detected on whichever digest column the auth stack uses (password_digest / encrypted_password).

Constant Summary collapse

PASSWORD_COLUMNS =
%w[password_digest encrypted_password].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.polymorphic_sessions?Boolean

Returns:

  • (Boolean)


48
49
50
51
52
# File 'lib/sessions/models/concerns/has_sessions.rb', line 48

def self.polymorphic_sessions?
  Sessions.session_model.column_names.include?("user_type")
rescue StandardError
  false
end

Instance Method Details

#revoke_all_sessions!(by: nil, reason: :admin_revoked) ⇒ Object

The admin hammer — the account-takeover response. Revokes EVERYTHING, including the session serving this request if it belongs to this user.



86
87
88
89
# File 'lib/sessions/models/concerns/has_sessions.rb', line 86

def revoke_all_sessions!(by: nil, reason: :admin_revoked)
  sessions.each { |session| session.revoke!(reason: reason, by: by) }
  true
end

#revoke_other_sessions!(current: nil, by: nil, reason: :logout_everywhere) ⇒ Object

GitHub’s “sign out everywhere else”: revoke every session except current (defaulting to the one serving this request, so a controller can call it bare). Each revocation writes its event and fires hooks.



75
76
77
78
79
80
81
82
# File 'lib/sessions/models/concerns/has_sessions.rb', line 75

def revoke_other_sessions!(current: nil, by: nil, reason: :logout_everywhere)
  current = Sessions.current if current.nil?

  scope = sessions
  scope = scope.where.not(id: current.id) if current.respond_to?(:id)
  scope.each { |session| session.revoke!(reason: reason, by: by || self) }
  true
end

#session_historyObject

The user’s COMPLETE trail slice — owned events PLUS the failed attempts typed against their email. Failures deliberately never link to accounts (‘session_events` alone can’t see them — recording a failure must not confirm an account exists); matching the resolved user’s own identity here is the safe read side. This is what the engine’s history page renders:

user.session_history.recent          # everything, newest first
user.session_history.failed_logins  # including identity-matched ones


63
64
65
66
67
68
69
70
# File 'lib/sessions/models/concerns/has_sessions.rb', line 63

def session_history
  scope = Sessions::Event.where(authenticatable: self)

  identity = Sessions::Event.normalize_identity(try(:email_address) || try(:email))
  scope = scope.or(Sessions::Event.where(identity: identity)) if identity

  scope
end