sessions 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -2
- data/README.md +9 -9
- data/app/controllers/sessions/application_controller.rb +2 -2
- data/app/views/sessions/_devices.html.erb +1 -1
- data/docs/PRD.md +52 -52
- data/docs/research/{01-carhey.md → 01-host-app.md} +23 -23
- data/docs/research/02-ecosystem.md +1 -1
- data/docs/research/07-device-detection.md +13 -13
- data/lib/generators/sessions/install_generator.rb +1 -1
- data/lib/generators/sessions/madmin_generator.rb +5 -5
- data/lib/generators/sessions/templates/add_lifecycle_to_sessions.rb.erb +38 -0
- data/lib/generators/sessions/templates/add_sessions_columns.rb.erb +26 -0
- data/lib/generators/sessions/templates/create_sessions.rb.erb +17 -5
- data/lib/generators/sessions/templates/create_sessions_events.rb.erb +4 -4
- data/lib/generators/sessions/templates/initializer.rb +2 -2
- data/lib/generators/sessions/templates/madmin/session_resource.rb +16 -8
- data/lib/generators/sessions/templates/madmin/sessions_controller.rb +4 -4
- data/lib/generators/sessions/upgrade_generator.rb +4 -1
- data/lib/sessions/adapters/omakase.rb +62 -18
- data/lib/sessions/adapters/warden.rb +163 -48
- data/lib/sessions/configuration.rb +4 -3
- data/lib/sessions/current.rb +4 -4
- data/lib/sessions/end_reason.rb +67 -0
- data/lib/sessions/models/concerns/has_sessions.rb +3 -3
- data/lib/sessions/models/concerns/model.rb +124 -59
- data/lib/sessions/models/event.rb +24 -17
- data/lib/sessions/version.rb +1 -1
- data/lib/sessions.rb +13 -12
- metadata +4 -2
|
@@ -8,10 +8,12 @@ module Sessions
|
|
|
8
8
|
# explicitly. Either way, ALL gem logic lives here, so the host's model
|
|
9
9
|
# file never goes stale.
|
|
10
10
|
#
|
|
11
|
-
# One mental model: **rows =
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# One mental model: **rows = session lifecycle state; events = audit
|
|
12
|
+
# history.** `ended_at: nil` means the device is live. Ending a row is an
|
|
13
|
+
# explicit state transition (`ended_reason`) and never relies on deleting a
|
|
14
|
+
# row plus later interpreting the absence. That distinction matters in
|
|
15
|
+
# Devise/Warden mode, where the gem decorates the host session and must
|
|
16
|
+
# never kick a user unless a durable explicit ending says so.
|
|
15
17
|
#
|
|
16
18
|
# The three lifecycle callbacks observe 100% of both adapters' flows:
|
|
17
19
|
#
|
|
@@ -20,7 +22,8 @@ module Sessions
|
|
|
20
22
|
# method, geolocate (when free).
|
|
21
23
|
# after_create_commit — write the `login` event, detect new devices,
|
|
22
24
|
# enforce the per-user cap, enqueue async geo.
|
|
23
|
-
# after_destroy_commit —
|
|
25
|
+
# after_destroy_commit — best-effort compatibility for host-side deletes
|
|
26
|
+
# (account erasure, Rails generator destroy_all).
|
|
24
27
|
#
|
|
25
28
|
# Every callback body is error-isolated: a parsing/geo/event failure may
|
|
26
29
|
# lose a log row; it may NEVER break a sign-in.
|
|
@@ -50,9 +53,9 @@ module Sessions
|
|
|
50
53
|
end
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
# Transient
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
+
# Transient direct-delete context: legacy/host destroy paths can still
|
|
57
|
+
# label their compatibility event. Normal gem APIs call `end!` and keep
|
|
58
|
+
# the row as durable lifecycle state.
|
|
56
59
|
attr_accessor :revocation_reason, :revoked_by
|
|
57
60
|
|
|
58
61
|
# Set on adopted rows (sessions that predate the gem) so adoption
|
|
@@ -61,24 +64,30 @@ module Sessions
|
|
|
61
64
|
|
|
62
65
|
before_create :sessions_enrich
|
|
63
66
|
after_create_commit :sessions_record_login
|
|
64
|
-
after_destroy_commit :
|
|
67
|
+
after_destroy_commit :sessions_record_destroyed_end
|
|
65
68
|
|
|
66
69
|
scope :by_recency, lambda {
|
|
67
70
|
# COALESCE keeps never-touched sessions ordered by creation time,
|
|
68
71
|
# portably across sqlite/postgres/mysql.
|
|
69
72
|
order(Arel.sql("COALESCE(#{table_name}.last_seen_at, #{table_name}.created_at) DESC"))
|
|
70
73
|
}
|
|
74
|
+
scope :live, lambda {
|
|
75
|
+
column_names.include?("ended_at") ? where(ended_at: nil) : all
|
|
76
|
+
}
|
|
77
|
+
scope :ended, lambda {
|
|
78
|
+
column_names.include?("ended_at") ? where.not(ended_at: nil) : none
|
|
79
|
+
}
|
|
71
80
|
scope :active, lambda {
|
|
72
|
-
where("COALESCE(#{table_name}.last_seen_at, #{table_name}.created_at) >= ?", INACTIVE_AFTER.ago)
|
|
81
|
+
live.where("COALESCE(#{table_name}.last_seen_at, #{table_name}.created_at) >= ?", INACTIVE_AFTER.ago)
|
|
73
82
|
}
|
|
74
83
|
scope :inactive, lambda {
|
|
75
|
-
where("COALESCE(#{table_name}.last_seen_at, #{table_name}.created_at) < ?", INACTIVE_AFTER.ago)
|
|
84
|
+
live.where("COALESCE(#{table_name}.last_seen_at, #{table_name}.created_at) < ?", INACTIVE_AFTER.ago)
|
|
76
85
|
}
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
class_methods do
|
|
80
89
|
# The user-facing trail rows linked to this registry (`session_id` is
|
|
81
|
-
# a plain column, no FK — history must survive row
|
|
90
|
+
# a plain column, no FK — history must survive lifecycle-row cleanup).
|
|
82
91
|
def sessions_events_for(session_id)
|
|
83
92
|
Sessions::Event.where(session_id: session_id)
|
|
84
93
|
end
|
|
@@ -137,15 +146,35 @@ module Sessions
|
|
|
137
146
|
|
|
138
147
|
# --- Revocation -----------------------------------------------------------
|
|
139
148
|
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# `
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
# End this registry row without deleting it. This is the core v0.2
|
|
150
|
+
# invariant: the row itself is the durable liveness state, and
|
|
151
|
+
# `sessions_events` remains a read-only audit trail.
|
|
152
|
+
def end!(reason:, by: nil, at: Time.current, metadata: nil, notify: true)
|
|
153
|
+
reason = Sessions::EndReason.normalize(reason)
|
|
154
|
+
event = nil
|
|
155
|
+
ended_now = false
|
|
156
|
+
|
|
157
|
+
with_lock do
|
|
158
|
+
unless ended?
|
|
159
|
+
sessions_assign_end_state(reason: reason, by: by, at: at, metadata: metadata)
|
|
160
|
+
save!
|
|
161
|
+
ended_now = true
|
|
162
|
+
event = sessions_record_end_event!(reason: reason, by: by, notify: false)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
Sessions.notify_event(event) if notify && event
|
|
167
|
+
@sessions_ended_now = ended_now
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# End this session as an explicit revocation/expiry action. In Devise mode
|
|
172
|
+
# the next request still proves possession of the per-row token before the
|
|
173
|
+
# adapter kicks; in Rails-8 auth mode the row id in the signed cookie now
|
|
174
|
+
# resolves to an ended row and the controller hook refuses it.
|
|
145
175
|
def revoke!(reason: :user_revoked, by: nil)
|
|
146
|
-
|
|
147
|
-
self
|
|
148
|
-
destroy!
|
|
176
|
+
end!(reason: reason, by: by)
|
|
177
|
+
return self unless @sessions_ended_now
|
|
149
178
|
|
|
150
179
|
Sessions.safely("revoke_remember_me") { sessions_forget_remember_me! } if Sessions.config.revoke_remember_me
|
|
151
180
|
Sessions.safely("on_session_revoked hook") do
|
|
@@ -159,6 +188,8 @@ module Sessions
|
|
|
159
188
|
|
|
160
189
|
# Opt-in expiry — false unless the host configured timeouts.
|
|
161
190
|
def sessions_expired?(now = Time.current)
|
|
191
|
+
return false if ended?
|
|
192
|
+
|
|
162
193
|
config = Sessions.config
|
|
163
194
|
activity = last_active_at
|
|
164
195
|
return true if config.idle_timeout && activity && activity < now - config.idle_timeout
|
|
@@ -167,6 +198,18 @@ module Sessions
|
|
|
167
198
|
false
|
|
168
199
|
end
|
|
169
200
|
|
|
201
|
+
def live?
|
|
202
|
+
!sessions_column?("ended_at") || try(:ended_at).blank?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def ended?
|
|
206
|
+
!live?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def sessions_kicks_on_resume?
|
|
210
|
+
ended? && Sessions::EndReason.kicks_on_resume?(try(:ended_reason))
|
|
211
|
+
end
|
|
212
|
+
|
|
170
213
|
# The throttled last-seen touch: at most one write per
|
|
171
214
|
# config.touch_every per session, issued as a single conditional UPDATE
|
|
172
215
|
# (hot-row-safe under concurrent requests, callback-free, and it also
|
|
@@ -176,6 +219,7 @@ module Sessions
|
|
|
176
219
|
every = Sessions.config.touch_every
|
|
177
220
|
return false unless every
|
|
178
221
|
return false unless sessions_column?("last_seen_at")
|
|
222
|
+
return false if ended?
|
|
179
223
|
|
|
180
224
|
now = Time.current
|
|
181
225
|
threshold = now - every
|
|
@@ -331,6 +375,24 @@ module Sessions
|
|
|
331
375
|
false
|
|
332
376
|
end
|
|
333
377
|
|
|
378
|
+
def sessions_assign_end_state(reason:, by:, at:, metadata:)
|
|
379
|
+
sessions_assign_force(ended_at: at, ended_reason: reason)
|
|
380
|
+
sessions_assign_force(ended_by_type: by.class.name) if by && sessions_column?("ended_by_type")
|
|
381
|
+
sessions_assign_force(ended_by_id: by.id) if by.respond_to?(:id) && sessions_column?("ended_by_id")
|
|
382
|
+
|
|
383
|
+
payload = metadata.presence || sessions_revoked_by_metadata(by)
|
|
384
|
+
sessions_assign_force(ended_metadata: payload) if payload && sessions_column?("ended_metadata")
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def sessions_assign_force(attributes)
|
|
388
|
+
attributes.each do |column, value|
|
|
389
|
+
name = column.to_s
|
|
390
|
+
next unless sessions_column?(name)
|
|
391
|
+
|
|
392
|
+
self[name] = value
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
334
396
|
# --- after_create_commit: the login event ------------------------------------
|
|
335
397
|
|
|
336
398
|
def sessions_record_login
|
|
@@ -361,8 +423,8 @@ module Sessions
|
|
|
361
423
|
auth_provider: try(:auth_provider),
|
|
362
424
|
auth_detail: try(:auth_detail).presence,
|
|
363
425
|
# The browser-continuity id rides the trail too: it's what lets
|
|
364
|
-
# Sessions.last_login answer "how did this browser last sign
|
|
365
|
-
#
|
|
426
|
+
# Sessions.last_login answer "how did this browser last sign in"
|
|
427
|
+
# after logout ends the row (the "Last used" badge).
|
|
366
428
|
device_id: try(:device_id).presence,
|
|
367
429
|
metadata: new_device ? { "new_device" => true } : nil
|
|
368
430
|
)
|
|
@@ -399,17 +461,16 @@ module Sessions
|
|
|
399
461
|
end
|
|
400
462
|
|
|
401
463
|
# The dedup half of browser continuity: prior live rows for the SAME
|
|
402
|
-
# user on the SAME browser install are superseded by this login.
|
|
403
|
-
# quiet
|
|
404
|
-
# remember-me rotation, no trail event (
|
|
405
|
-
#
|
|
406
|
-
#
|
|
464
|
+
# user on the SAME browser install are superseded by this login. This is
|
|
465
|
+
# a quiet lifecycle end on purpose — no on_session_revoked hook, no
|
|
466
|
+
# remember-me rotation, no trail event (housekeeping, not a security
|
|
467
|
+
# event). Scoped to the user: a shared computer with two accounts keeps
|
|
468
|
+
# both rows.
|
|
407
469
|
def sessions_supersede_previous_rows!
|
|
408
470
|
return if try(:device_id).blank?
|
|
409
471
|
|
|
410
|
-
self.class.where(user: user, device_id: device_id).where.not(id: id).each do |row|
|
|
411
|
-
row.
|
|
412
|
-
row.destroy
|
|
472
|
+
self.class.live.where(user: user, device_id: device_id).where.not(id: id).each do |row|
|
|
473
|
+
row.end!(reason: :superseded, notify: false)
|
|
413
474
|
end
|
|
414
475
|
end
|
|
415
476
|
|
|
@@ -420,7 +481,7 @@ module Sessions
|
|
|
420
481
|
cap = Sessions.config.max_sessions_per_user
|
|
421
482
|
return unless cap
|
|
422
483
|
|
|
423
|
-
siblings = self.class.where(user: user)
|
|
484
|
+
siblings = self.class.live.where(user: user)
|
|
424
485
|
overflow = siblings.count - cap
|
|
425
486
|
return unless overflow.positive?
|
|
426
487
|
|
|
@@ -429,46 +490,50 @@ module Sessions
|
|
|
429
490
|
end
|
|
430
491
|
end
|
|
431
492
|
|
|
432
|
-
# --- after_destroy_commit:
|
|
493
|
+
# --- after_destroy_commit: legacy/direct-delete compatibility -----------------
|
|
433
494
|
|
|
434
|
-
def
|
|
435
|
-
|
|
436
|
-
# Account deletion: dependent-destroyed rows of a destroyed owner
|
|
437
|
-
# write no events (their trail is erased with them — GDPR default).
|
|
438
|
-
next if user.nil? || user.destroyed?
|
|
495
|
+
def sessions_record_destroyed_end
|
|
496
|
+
return if ended?
|
|
439
497
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
else "revoked"
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
Sessions::Event.record!(
|
|
450
|
-
sessions_event_identity_attributes.merge(
|
|
451
|
-
event: event_name,
|
|
452
|
-
revoked_reason: (event_name == "revoked" ? (reason || :unknown) : nil),
|
|
453
|
-
metadata: sessions_revoked_by_metadata
|
|
454
|
-
)
|
|
455
|
-
)
|
|
498
|
+
Sessions.safely("record_end") do
|
|
499
|
+
# Account deletion: dependent-destroyed rows of a destroyed owner write
|
|
500
|
+
# no events (their trail is erased with them — GDPR default). Direct
|
|
501
|
+
# host deletes are otherwise treated as legacy explicit ends; v0.2's
|
|
502
|
+
# public APIs call `end!`/`revoke!` and preserve the row.
|
|
503
|
+
sessions_record_end_event!(reason: revocation_reason || :unknown, by: revoked_by)
|
|
456
504
|
end
|
|
457
505
|
end
|
|
458
506
|
|
|
459
|
-
def
|
|
460
|
-
return nil
|
|
507
|
+
def sessions_record_end_event!(reason: nil, by: nil, notify: true)
|
|
508
|
+
return if user.nil? || user.destroyed?
|
|
509
|
+
|
|
510
|
+
reason = Sessions::EndReason.normalize(reason || revocation_reason)
|
|
511
|
+
event_name = Sessions::EndReason.event_for(reason)
|
|
512
|
+
return unless event_name
|
|
513
|
+
|
|
514
|
+
Sessions::Event.record_strict!(
|
|
515
|
+
sessions_event_identity_attributes.merge(
|
|
516
|
+
event: event_name,
|
|
517
|
+
revoked_reason: Sessions::EndReason.revoked_reason_for(reason),
|
|
518
|
+
metadata: sessions_revoked_by_metadata(by || revoked_by)
|
|
519
|
+
),
|
|
520
|
+
notify: notify
|
|
521
|
+
)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def sessions_revoked_by_metadata(actor = revoked_by)
|
|
525
|
+
return nil unless actor
|
|
461
526
|
|
|
462
|
-
label = if
|
|
463
|
-
"#{
|
|
527
|
+
label = if actor.respond_to?(:id) && actor.class.respond_to?(:name)
|
|
528
|
+
"#{actor.class.name}##{actor.id}"
|
|
464
529
|
else
|
|
465
|
-
|
|
530
|
+
actor.to_s
|
|
466
531
|
end
|
|
467
532
|
{ "revoked_by" => label }
|
|
468
533
|
end
|
|
469
534
|
|
|
470
535
|
# The row's identity, copied onto every event it produces — the trail
|
|
471
|
-
# must describe the device even after the row is
|
|
536
|
+
# must describe the device even after the row is ended or later erased.
|
|
472
537
|
def sessions_event_identity_attributes
|
|
473
538
|
{
|
|
474
539
|
session_id: id,
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module Sessions
|
|
4
4
|
# The append-only login-activity trail: every successful AND failed login,
|
|
5
5
|
# logout, revocation and expiry — with attempted identity, device, geo,
|
|
6
|
-
# and the linkage no prior art has: `session_id` points at the
|
|
6
|
+
# and the linkage no prior art has: `session_id` points at the lifecycle
|
|
7
7
|
# registry row the event created (or ended), so a suspicious login in the
|
|
8
8
|
# trail is one click away from revoking the session it started.
|
|
9
9
|
#
|
|
10
|
-
# `session_id` is a plain column with NO foreign key on purpose:
|
|
11
|
-
#
|
|
12
|
-
# must survive
|
|
10
|
+
# `session_id` is a plain column with NO foreign key on purpose: hosts may
|
|
11
|
+
# still hard-delete rows for account erasure or legacy Rails destroy_all
|
|
12
|
+
# flows, and history must survive that. Normal v0.2 logout/revocation ends
|
|
13
|
+
# the row in place and records an event as audit, not liveness state.
|
|
13
14
|
#
|
|
14
15
|
# Rows are written through one tolerant pipeline (`.record!`): unknown
|
|
15
16
|
# attributes are dropped instead of raising, so hosts can add or remove
|
|
@@ -85,20 +86,26 @@ module Sessions
|
|
|
85
86
|
# tees the event into `config.events`. Returns the Event or nil —
|
|
86
87
|
# never raises into a login.
|
|
87
88
|
def record!(attributes)
|
|
88
|
-
Sessions.safely("event")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
event
|
|
89
|
+
Sessions.safely("event") { record_strict!(attributes) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Same tolerant attribute pipeline as `record!`, but persistence errors
|
|
93
|
+
# bubble. `Session#end!` writes lifecycle state and its matching audit
|
|
94
|
+
# event in one transaction; if the event cannot be written, the row must
|
|
95
|
+
# stay live rather than silently losing the audit trail.
|
|
96
|
+
def record_strict!(attributes, notify: true)
|
|
97
|
+
event = new
|
|
98
|
+
attributes.each do |name, value|
|
|
99
|
+
next if value.nil?
|
|
100
|
+
|
|
101
|
+
event.try(:"#{name}=", value)
|
|
101
102
|
end
|
|
103
|
+
event.identity = normalize_identity(event.try(:identity))
|
|
104
|
+
clamp_string_columns!(event)
|
|
105
|
+
event.save!
|
|
106
|
+
|
|
107
|
+
Sessions.notify_event(event) if notify
|
|
108
|
+
event
|
|
102
109
|
end
|
|
103
110
|
|
|
104
111
|
# Clamp string columns to their limits BEFORE the insert: the
|
data/lib/sessions/version.rb
CHANGED
data/lib/sessions.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "sessions/version"
|
|
|
14
14
|
require_relative "sessions/errors"
|
|
15
15
|
require_relative "sessions/configuration"
|
|
16
16
|
require_relative "sessions/current"
|
|
17
|
+
require_relative "sessions/end_reason"
|
|
17
18
|
require_relative "sessions/ip_address"
|
|
18
19
|
require_relative "sessions/device"
|
|
19
20
|
require_relative "sessions/classifier"
|
|
@@ -36,7 +37,7 @@ require_relative "sessions/engine" if defined?(::Rails::Engine)
|
|
|
36
37
|
# Sessions.configure { |config| ... } # one block, in an initializer
|
|
37
38
|
# has_sessions # on your auth model
|
|
38
39
|
#
|
|
39
|
-
# current_user.sessions.
|
|
40
|
+
# current_user.sessions.live # live devices
|
|
40
41
|
# session.device_name # => "Chrome on macOS"
|
|
41
42
|
# session.revoke! # remote logout, effective next request
|
|
42
43
|
# current_user.revoke_other_sessions! # GitHub's "sign out everywhere else"
|
|
@@ -245,7 +246,7 @@ module Sessions
|
|
|
245
246
|
}
|
|
246
247
|
end
|
|
247
248
|
|
|
248
|
-
# Right-to-erasure helper: destroy every
|
|
249
|
+
# Right-to-erasure helper: destroy every session row, delete the trail,
|
|
249
250
|
# and null the typed identity on any retained failure rows that match
|
|
250
251
|
# +user+'s email — so honoring a GDPR deletion request is one call.
|
|
251
252
|
def forget(user, identity: nil)
|
|
@@ -327,11 +328,11 @@ module Sessions
|
|
|
327
328
|
request.session.to_hash.each do |key, value|
|
|
328
329
|
next unless key.to_s.match?(pattern) && value.is_a?(Hash)
|
|
329
330
|
|
|
330
|
-
|
|
331
|
-
next unless
|
|
331
|
+
tracking = Adapters::Warden.parse_tracking_state(value[Adapters::Warden::SESSION_KEY])
|
|
332
|
+
next unless tracking && tracking[:mode] == "credential"
|
|
332
333
|
|
|
333
|
-
row = session_model.find_by(id: id)
|
|
334
|
-
return row if row.respond_to?(:sessions_token_matches?) && row&.sessions_token_matches?(token)
|
|
334
|
+
row = session_model.live.find_by(id: tracking[:id])
|
|
335
|
+
return row if row.respond_to?(:sessions_token_matches?) && row&.sessions_token_matches?(tracking[:token])
|
|
335
336
|
end
|
|
336
337
|
nil
|
|
337
338
|
rescue StandardError
|
|
@@ -342,7 +343,7 @@ module Sessions
|
|
|
342
343
|
return nil unless request.respond_to?(:cookie_jar)
|
|
343
344
|
|
|
344
345
|
id = request.cookie_jar.signed[:session_id]
|
|
345
|
-
session_model.find_by(id: id) if id
|
|
346
|
+
session_model.live.find_by(id: id) if id
|
|
346
347
|
rescue StandardError
|
|
347
348
|
nil
|
|
348
349
|
end
|
|
@@ -374,12 +375,12 @@ module Sessions
|
|
|
374
375
|
threshold = idle.ago
|
|
375
376
|
# A session's last activity is its throttled touch when present,
|
|
376
377
|
# else its creation.
|
|
377
|
-
scopes << session_model.where(last_seen_at: ...threshold)
|
|
378
|
-
scopes << session_model.where(last_seen_at: nil).where(created_at: ...threshold)
|
|
378
|
+
scopes << session_model.live.where(last_seen_at: ...threshold)
|
|
379
|
+
scopes << session_model.live.where(last_seen_at: nil).where(created_at: ...threshold)
|
|
379
380
|
end
|
|
380
381
|
|
|
381
382
|
if (lifetime = config.max_session_lifetime)
|
|
382
|
-
scopes << session_model.where(created_at: ...lifetime.ago)
|
|
383
|
+
scopes << session_model.live.where(created_at: ...lifetime.ago)
|
|
383
384
|
end
|
|
384
385
|
|
|
385
386
|
# NOTE: reduce(:or), never `none.or(...)` — a NullRelation stays null
|
|
@@ -398,10 +399,10 @@ module Sessions
|
|
|
398
399
|
owner_keys = session_model.column_names.include?("user_type") ? %i[user_type user_id] : %i[user_id]
|
|
399
400
|
|
|
400
401
|
count = 0
|
|
401
|
-
session_model.group(*owner_keys).count.each do |owner_key, sessions_count|
|
|
402
|
+
session_model.live.group(*owner_keys).count.each do |owner_key, sessions_count|
|
|
402
403
|
next if sessions_count <= cap
|
|
403
404
|
|
|
404
|
-
session_model.where(owner_keys.zip(Array(owner_key)).to_h)
|
|
405
|
+
session_model.live.where(owner_keys.zip(Array(owner_key)).to_h)
|
|
405
406
|
.order(created_at: :asc)
|
|
406
407
|
.limit(sessions_count - cap)
|
|
407
408
|
.each do |session|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sessions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
@@ -148,7 +148,7 @@ files:
|
|
|
148
148
|
- config/locales/es.yml
|
|
149
149
|
- config/routes.rb
|
|
150
150
|
- docs/PRD.md
|
|
151
|
-
- docs/research/01-
|
|
151
|
+
- docs/research/01-host-app.md
|
|
152
152
|
- docs/research/02-ecosystem.md
|
|
153
153
|
- docs/research/03-rails-core.md
|
|
154
154
|
- docs/research/04-devise-warden.md
|
|
@@ -165,6 +165,7 @@ files:
|
|
|
165
165
|
- lib/generators/sessions/madmin_generator.rb
|
|
166
166
|
- lib/generators/sessions/templates/add_adoption_key_to_sessions.rb.erb
|
|
167
167
|
- lib/generators/sessions/templates/add_app_build_to_sessions_events.rb.erb
|
|
168
|
+
- lib/generators/sessions/templates/add_lifecycle_to_sessions.rb.erb
|
|
168
169
|
- lib/generators/sessions/templates/add_sessions_columns.rb.erb
|
|
169
170
|
- lib/generators/sessions/templates/create_sessions.rb.erb
|
|
170
171
|
- lib/generators/sessions/templates/create_sessions_events.rb.erb
|
|
@@ -185,6 +186,7 @@ files:
|
|
|
185
186
|
- lib/sessions/configuration.rb
|
|
186
187
|
- lib/sessions/current.rb
|
|
187
188
|
- lib/sessions/device.rb
|
|
189
|
+
- lib/sessions/end_reason.rb
|
|
188
190
|
- lib/sessions/engine.rb
|
|
189
191
|
- lib/sessions/errors.rb
|
|
190
192
|
- lib/sessions/geolocation.rb
|