Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.7.4
3.4.4
31 changes: 17 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ GEM
nio4r (2.7.5)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
orm_adapter (0.5.0)
Expand All @@ -160,14 +162,14 @@ GEM
date
stringio
racc (1.8.1)
rack (2.2.21)
rack-session (1.0.2)
rack (< 3)
rack (3.2.4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (1.0.1)
rack (< 3)
webrick
rackup (2.2.1)
rack (>= 3)
rails (7.2.3)
actioncable (= 7.2.3)
actionmailbox (= 7.2.3)
Expand Down Expand Up @@ -208,9 +210,9 @@ GEM
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
Expand All @@ -223,10 +225,10 @@ GEM
rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.5)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-rails (8.0.2)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
Expand Down Expand Up @@ -261,6 +263,7 @@ GEM
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.8.0-aarch64-linux-gnu)
sqlite3 (2.8.0-arm64-darwin)
sqlite3 (2.8.0-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand All @@ -274,7 +277,6 @@ GEM
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
webrick (1.9.1)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
Expand All @@ -283,6 +285,7 @@ GEM

PLATFORMS
aarch64-linux
arm64-darwin-24
x86_64-linux

DEPENDENCIES
Expand Down
12 changes: 12 additions & 0 deletions lib/devise_last_seen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ module Devise

# Attribute who will be updated every time a user is set by the Warden's after_save callback
mattr_accessor :last_seen_at_attribute, default: :last_seen

# Whether to track the last seen session, if enabled, the gem will create a new session in history
# after the session duration is reached.
mattr_accessor :last_seen_at_enable_session_tracking, default: false

# Duration (in seconds) to keep the last seen session open,
# after that it will generate a new session in history.
mattr_accessor :last_seen_at_session_duration, default: 1.day
end

if Devise.last_seen_at_enable_session_tracking && defined?(ActiveRecord) && !defined?(DeviseLastSeen::Session)
require 'devise_last_seen/session'
end

module DeviseLastSeen; end
Expand Down
8 changes: 8 additions & 0 deletions lib/devise_last_seen/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ def track_last_seen!

public_send(last_seen_at_attribute_writer, DateTime.now)

track_last_seen_session!

save(validate: false)
end

def last_seen_at_attribute_writer
@last_seen_at_attribute_writer ||= :"#{Devise.last_seen_at_attribute}="
end

def track_last_seen_session!
return unless Devise.last_seen_at_enable_session_tracking && defined?(DeviseLastSeen::Session)

DeviseLastSeen::Session.create_for(self)
end
end
end
end
51 changes: 51 additions & 0 deletions lib/devise_last_seen/session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module DeviseLastSeen
class Session < ::ActiveRecord::Base
# Check session_duration configuration when Session class is loaded
if Devise.last_seen_at_enable_session_tracking && Devise.last_seen_at_session_duration.blank?
Rails.logger.warn(
'[devise_last_seen] WARNING: Devise.last_seen_at_session_duration is ' \
'nil or blank. Set it to a positive integer to avoid creating too many sessions.'
)
end

self.table_name = 'devise_last_seen_sessions'

belongs_to :user, polymorphic: true

validates :user_id, presence: true
validates :user_type, presence: true
validates :last_seen_at, presence: true

scope :last_seen_at, -> { order(last_seen_at: :desc) }
scope :active, -> { last_seen_at.where('expires_at > ?', Time.current) }

def self.create_for(record)
current_session = active.where(user: record).first

return update_current_session(current_session) if current_session.present?

create_new_session(record)
end

def self.update_current_session(current_session)
current_session.update(last_seen_at: Time.current)
end

def self.create_new_session(record)
max_session_time = Time.current + Devise.last_seen_at_session_duration.to_i

create!(
user: record,
started_at: Time.current,
last_seen_at: max_session_time,
expires_at: max_session_time
)
rescue StandardError => e
Rails.logger.error("[devise_last_seen] Error creating new session for #{record.class.name}: #{e.message}")

nil
end
end
end
97 changes: 97 additions & 0 deletions spec/devise_last_seen/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,101 @@ def save(validate: nil)
end
end
end

context 'with session tracking enabled', type: :feature do
before(:all) do
require 'integration_spec_helper'
end

let(:user) { User.create(email: 'test@example.com', password: 'password123', name: 'Test User') }

before do
Devise.setup do |config|
config.last_seen_at_enable_session_tracking = true
config.last_seen_at_session_duration = 1.day
end
end

after do
Devise.setup do |config|
config.last_seen_at_enable_session_tracking = false
config.last_seen_at_session_duration = 1.day
end
end

describe '#track_last_seen!' do
context 'when there is no active session' do
it 'creates a new session' do
expect do
user.track_last_seen!
end.to change(DeviseLastSeen::Session, :count).by(1)
end

it 'creates a session with correct attributes' do
freeze_time = Time.current
allow(Time).to receive(:current).and_return(freeze_time)

user.track_last_seen!

session = DeviseLastSeen::Session.last
expect(session).to be_present
expect(session.user).to eq(user)
expect(session.started_at).to be_within(1.second).of(freeze_time)
end
end

context 'when there is an active session' do
let!(:existing_session) do
DeviseLastSeen::Session.create!(
user: user,
started_at: 1.hour.ago,
last_seen_at: 1.hour.ago,
expires_at: 1.hour.from_now
)
end

it 'does not create a new session' do
expect do
user.track_last_seen!
end.not_to change(DeviseLastSeen::Session, :count)
end

it 'updates the existing session last_seen_at' do
freeze_time = Time.current
allow(Time).to receive(:current).and_return(freeze_time)

user.track_last_seen!

existing_session.reload
expect(existing_session.last_seen_at).to be_within(1.second).of(freeze_time)
end
end

context 'when session tracking is disabled' do
before do
Devise.setup do |config|
config.last_seen_at_enable_session_tracking = false
end
end

it 'does not create a session' do
expect do
user.track_last_seen!
end.not_to change(DeviseLastSeen::Session, :count)
end
end

context 'when time passed is lower than the interval' do
let(:user) do
User.create(email: 'test@example.com', password: 'password123', name: 'Test User', last_seen: 2.minutes.ago)
end

it 'does not create a session' do
expect do
user.track_last_seen!
end.not_to change(DeviseLastSeen::Session, :count)
end
end
end
end
end
Loading
Loading