Skip to content
Open
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
7 changes: 7 additions & 0 deletions lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ module Test
mattr_accessor :timeout_in
@@timeout_in = 30.minutes

# Minimum interval between session writes when using :timeoutable. When set,
# last_request_at is only updated if more than this interval has elapsed since
# the last write, reducing database writes on busy applications. Defaults to
# nil (write on every request, preserving original behaviour).
mattr_accessor :last_request_at_update_interval
@@last_request_at_update_interval = nil

# Used to hash the password. Please generate one with rails secret.
mattr_accessor :pepper
@@pepper = nil
Expand Down
5 changes: 4 additions & 1 deletion lib/devise/hooks/timeoutable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
end

unless env['devise.skip_trackable']
warden.session(scope)['last_request_at'] = Time.now.utc.to_i
interval = record.last_request_at_update_interval
if interval.nil? || last_request_at.nil? || (Time.now.utc - last_request_at) >= interval
warden.session(scope)['last_request_at'] = Time.now.utc.to_i
end
end
end
end
9 changes: 8 additions & 1 deletion lib/devise/models/timeoutable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ module Models
# Timeoutable adds the following options to +devise+:
#
# * +timeout_in+: the interval to timeout the user session without activity.
# * +last_request_at_update_interval+: the minimum time that must pass before
# last_request_at is written to the session again. Reduces database writes
# on busy apps. Defaults to nil (write on every request).
#
# == Examples
#
Expand All @@ -35,10 +38,14 @@ def timeout_in
self.class.timeout_in
end

def last_request_at_update_interval
self.class.last_request_at_update_interval
end

private

module ClassMethods
Devise::Models.config(self, :timeout_in)
Devise::Models.config(self, :timeout_in, :last_request_at_update_interval)
end
end
end
Expand Down
8 changes: 8 additions & 0 deletions lib/generators/templates/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@
# time the user will be asked for credentials again. Default is 30 minutes.
# config.timeout_in = 30.minutes

# Minimum interval between session writes for :timeoutable. When set,
# last_request_at is only updated if this much time has elapsed since the
# last write. Useful for reducing database writes on high-traffic apps (e.g.
# when using Devise::Timeoutable with a database-backed session store).
# Must be less than timeout_in to avoid premature session expiry.
# Default is nil (write on every request).
# config.last_request_at_update_interval = 1.minute

# ==> Configuration for :lockable
# Defines which strategy will be used to lock an account.
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
Expand Down
38 changes: 38 additions & 0 deletions test/integration/timeoutable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,44 @@ def last_request_at
assert warden.authenticated?(:user)
end

test 'does not update last_request_at within the last_request_at_update_interval' do
swap Devise, last_request_at_update_interval: 5.minutes do
sign_in_as_user
first_request_at = last_request_at
assert_not_nil first_request_at

get users_path
assert_equal first_request_at, last_request_at
end
end

test 'updates last_request_at after the last_request_at_update_interval has elapsed' do
swap Devise, last_request_at_update_interval: 5.minutes do
sign_in_as_user
first_request_at = last_request_at
assert_not_nil first_request_at

new_time = 6.minutes.from_now
Time.stubs(:now).returns(new_time)

get users_path
assert_not_equal first_request_at, last_request_at
end
end

test 'last_request_at_update_interval defaults to nil and writes on every request' do
assert_nil Devise.last_request_at_update_interval

sign_in_as_user
first_request_at = last_request_at

new_time = 10.seconds.from_now
Time.stubs(:now).returns(new_time)

get users_path
assert_not_equal first_request_at, last_request_at
end

test 'does not crash when the last_request_at is a String' do
user = sign_in_as_user

Expand Down