sessions 0.1.0 → 0.1.1
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 +17 -0
- data/README.md +5 -0
- data/app/controllers/sessions/application_controller.rb +10 -3
- data/app/helpers/sessions/engine_helper.rb +8 -1
- data/config/locales/en.yml +11 -0
- data/config/locales/es.yml +7 -0
- data/lib/generators/sessions/install_generator.rb +55 -0
- data/lib/generators/sessions/templates/create_sessions.rb.erb +15 -5
- data/lib/generators/sessions/templates/create_sessions_events.rb.erb +15 -6
- data/lib/sessions/adapters/warden.rb +33 -3
- data/lib/sessions/classifier.rb +7 -1
- data/lib/sessions/models/concerns/model.rb +8 -1
- data/lib/sessions/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a11c0aa8a096b0cc19e25a62967da2414cdf8732a2587cdd9a23ed7d989fd67
|
|
4
|
+
data.tar.gz: ea2e6a1fa8a96771886c86e74d6dfa391fffa4138e825006511222dd221652a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4aa37c48143f477dc820e85b8a44158682f4f40991411bb382af74fefd20d0001050dd2e7324830cea73e4edc459e252f3762aaea034bd4386bacda488f088c
|
|
7
|
+
data.tar.gz: d29228d847cb0acd7e6b348a872de1ae988afdbe5e70c69509f24a41aa18992109f57ba2964cddc66a08a6f09802cbdfe9a55d1a8d9fc4e6d01caeb92d9db6a8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 (2026-06-12)
|
|
4
|
+
|
|
5
|
+
Production-found fix, hours after 0.1.0: a client that forwards cookies **read-only** — the canonical case is a native app's HTTP layer that attaches the WebView's cookie but drops `Set-Cookie` responses — re-enters the Devise-mode *adoption* path on every request, because the session token the gem writes never persists client-side. Each pass minted a fresh adopted row (and adoption also skipped the per-user cap), so one phone on a polling screen accumulated hundreds of "live devices" in an hour.
|
|
6
|
+
|
|
7
|
+
- **Adoption is now idempotent**: a re-entering client matches its recent adopted row (same user, scope, and user agent within 24h) and just touches it — no new row, no token rotation (rotating would kick a sibling client validly holding that row's token, e.g. the app's WebView next to its native HTTP stack).
|
|
8
|
+
- **The per-user session cap now applies to suppressed (adopted) writes too** — it's the hard limit on live rows, whatever path creates them.
|
|
9
|
+
|
|
10
|
+
Plus a full-codebase audit pass:
|
|
11
|
+
|
|
12
|
+
- **The remote-revocation kick now ships its own flash copy.** The Warden adapter throws `message: :session_revoked`, and Devise's failure app resolves it via `devise.failure.session_revoked` — a key nothing shipped, so revoked devices saw a literal "Translation missing: en.devise.failure.user.session_revoked" at the exact moment the flagship feature fired. The gem now provides the key in English and Spanish (override it like any I18n key), pinned by a test that mirrors Devise's exact lookup (`I18n.t(:"#{scope}.#{message}", scope: "devise.failure", default: [message])`).
|
|
13
|
+
- **`config.strategy_methods` now truly overrides built-ins on a shared key.** The mapping merge let the built-in value win a duplicate key (e.g. remapping `"Rememberable"`), contradicting the documented host-wins contract; host entries now win both the iteration order and key conflicts.
|
|
14
|
+
- **Failed-login identity capture also reads `email_address`** — the omakase-era key, and what Devise apps with `authentication_keys = [:email_address]` post. Those failures used to record `identity: nil`, quietly gutting ATO triage and `repeated_failed_logins` for that setup.
|
|
15
|
+
- **The installer now validates the Devise auth model fit.** Default (non-polymorphic) mode assumes a `User` class — the same assumption `rails generate authentication` makes. A Devise app on `Member`/`Account` used to pass detection and then break at `db:migrate` (foreign key to a missing `users` table); the installer now reads `Devise.mappings` and stops with the fix (`--polymorphic`) before writing anything, and warns (without blocking) when extra scopes ride alongside `User`, since those stay silently untracked in default mode. Unreadable mappings stay permissive. README gained the matching callout.
|
|
16
|
+
- **API-only hosts boot.** The engine controller's class body used `helper`/`helper_method`/`layout` unconditionally — none of which exist on `ActionController::API` — and production eager-loads engine controllers whether or not the engine is mounted, so an API-only app bundling the gem for the model/trail APIs failed to boot. The view-layer DSL is now capability-guarded (mounting the HTML page still requires a `Base`-derived parent, as documented).
|
|
17
|
+
- **Generated migrations honor exotic primary-key types.** `primary_key_type: :string` (ULID-style apps) now flows through to the events table's PK and the `session_id` linkage column (previously coerced to `bigint`, breaking the trail↔registry join), and the serial pseudo-types map to their plain integer column equivalents for reference columns — a `t.bigserial` reference would mint its own sequence.
|
|
18
|
+
- `sessions_format_date`/`sessions_format_time` tolerate `nil` (custom views passing a nullable column used to hit `nil.strftime` *inside* the fallback rescue).
|
|
19
|
+
|
|
3
20
|
## 0.1.0 (2026-06-12)
|
|
4
21
|
|
|
5
22
|
First release — the missing session layer for Rails. 🔐
|
data/README.md
CHANGED
|
@@ -91,6 +91,11 @@ end
|
|
|
91
91
|
|
|
92
92
|
That's it. Every sign-in from now on lands on the devices page and in the trail — on Rails 8 auth there is literally nothing else to wire (the gem decorates the generated `Session` model automatically; your app code stays untouched).
|
|
93
93
|
|
|
94
|
+
> [!NOTE]
|
|
95
|
+
> **Auth model isn't `User`?** (Devise on `Member`/`Account`, or several `devise_for` scopes): install with `rails generate sessions:install --polymorphic` — the session owner becomes polymorphic and every scope/model gets tracked; put `has_sessions` on each of them. The default install assumes a `User` class (the same assumption `rails generate authentication` makes), and the installer stops with exactly this advice when your Devise mappings don't fit it.
|
|
96
|
+
>
|
|
97
|
+
> **API-only app?** The gem boots and tracks fine (use the model APIs and `Sessions.track_login`), but the devices page is HTML — mounting the engine needs an `ActionController::Base`-derived `config.parent_controller`, which API-only apps don't have.
|
|
98
|
+
|
|
94
99
|
## What `sessions` does (and doesn't) do
|
|
95
100
|
|
|
96
101
|
**Does:**
|
|
@@ -33,12 +33,19 @@ module Sessions
|
|
|
33
33
|
|
|
34
34
|
before_action :sessions_authenticate!
|
|
35
35
|
|
|
36
|
-
helper
|
|
37
|
-
|
|
36
|
+
# The view-layer DSL (`helper`, `helper_method`, `layout`) doesn't exist
|
|
37
|
+
# on ActionController::API — and an API-only host that bundles the gem
|
|
38
|
+
# purely for the model/trail APIs still EAGER LOADS this class in
|
|
39
|
+
# production, mounted or not. Guarding keeps such hosts bootable; the
|
|
40
|
+
# HTML devices page itself still requires a Base-derived
|
|
41
|
+
# config.parent_controller (the default), which is the only setup it's
|
|
42
|
+
# rendered under.
|
|
43
|
+
helper Sessions::EngineHelper if respond_to?(:helper)
|
|
44
|
+
helper_method :sessions_current_user, :sessions_current_session if respond_to?(:helper_method)
|
|
38
45
|
|
|
39
46
|
# nil falls through to the parent controller's regular layout
|
|
40
47
|
# resolution, so by default the page looks like the rest of the host.
|
|
41
|
-
layout :sessions_layout
|
|
48
|
+
layout :sessions_layout if respond_to?(:layout)
|
|
42
49
|
|
|
43
50
|
private
|
|
44
51
|
|
|
@@ -102,14 +102,21 @@ module Sessions
|
|
|
102
102
|
|
|
103
103
|
# "Signed in May 2, 2026" — localized when the host bundles date
|
|
104
104
|
# formats (rails-i18n or its own locale files), with a safe fallback so
|
|
105
|
-
# a bare host never 500s over a missing `date.formats.long`.
|
|
105
|
+
# a bare host never 500s over a missing `date.formats.long`. nil-safe:
|
|
106
|
+
# without the guard, I18n.l(nil) raises I18n::ArgumentError and the
|
|
107
|
+
# rescue would then call nil.strftime — a trap for custom views passing
|
|
108
|
+
# a nullable column.
|
|
106
109
|
def sessions_format_date(date)
|
|
110
|
+
return nil unless date
|
|
111
|
+
|
|
107
112
|
I18n.l(date, format: :long)
|
|
108
113
|
rescue I18n::MissingTranslationData, I18n::ArgumentError
|
|
109
114
|
date.strftime("%Y-%m-%d")
|
|
110
115
|
end
|
|
111
116
|
|
|
112
117
|
def sessions_format_time(time)
|
|
118
|
+
return nil unless time
|
|
119
|
+
|
|
113
120
|
I18n.l(time, format: :short)
|
|
114
121
|
rescue I18n::MissingTranslationData, I18n::ArgumentError
|
|
115
122
|
time.strftime("%Y-%m-%d %H:%M")
|
data/config/locales/en.yml
CHANGED
|
@@ -57,3 +57,14 @@ en:
|
|
|
57
57
|
composite: "%{client} on %{platform}"
|
|
58
58
|
unknown: "Unknown device"
|
|
59
59
|
bot: "Bot (%{name})"
|
|
60
|
+
# The flash a remotely-revoked device sees on its next request, rendered by
|
|
61
|
+
# Devise's failure app (the Warden adapter kicks with
|
|
62
|
+
# `throw :warden, message: :session_revoked`). Devise's lookup is
|
|
63
|
+
# `I18n.t(:"#{scope}.#{message}", scope: "devise.failure", default: [message])`
|
|
64
|
+
# — see Devise::FailureApp#i18n_message (devise/lib/devise/failure_app.rb).
|
|
65
|
+
# Without this key the flash literally reads
|
|
66
|
+
# "Translation missing: en.devise.failure.user.session_revoked".
|
|
67
|
+
# Hosts override it like any I18n key (app locale files load after engines).
|
|
68
|
+
devise:
|
|
69
|
+
failure:
|
|
70
|
+
session_revoked: "That session was signed out remotely. Please sign in again."
|
data/config/locales/es.yml
CHANGED
|
@@ -57,3 +57,10 @@ es:
|
|
|
57
57
|
composite: "%{client} en %{platform}"
|
|
58
58
|
unknown: "Dispositivo desconocido"
|
|
59
59
|
bot: "Bot (%{name})"
|
|
60
|
+
# El aviso que ve un dispositivo cuya sesión fue revocada en remoto, en su
|
|
61
|
+
# siguiente petición (lo muestra la failure app de Devise; el adaptador de
|
|
62
|
+
# Warden expulsa con `throw :warden, message: :session_revoked`). Sin esta
|
|
63
|
+
# clave, el flash mostraría literalmente "Translation missing: …".
|
|
64
|
+
devise:
|
|
65
|
+
failure:
|
|
66
|
+
session_revoked: "Esa sesión se cerró de forma remota. Vuelve a iniciar sesión."
|
|
@@ -78,6 +78,48 @@ module Sessions
|
|
|
78
78
|
MSG
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
# Default (non-polymorphic) mode assumes a `User` class — the same
|
|
82
|
+
# assumption `rails generate authentication` makes: `belongs_to :user`
|
|
83
|
+
# and a foreign key to `users`. A Devise app whose auth model is
|
|
84
|
+
# Member/Account would pass detection and then break at db:migrate
|
|
85
|
+
# (FK to a missing `users` table) or at runtime (`belongs_to :user`
|
|
86
|
+
# constantizing a class that doesn't exist) — catch it HERE, with the
|
|
87
|
+
# fix in the error message. `--polymorphic` works with any model(s).
|
|
88
|
+
def check_devise_auth_model_fit!
|
|
89
|
+
return if polymorphic?
|
|
90
|
+
return unless devise_detected?
|
|
91
|
+
# An adopted omakase table already proves whatever owner shape the
|
|
92
|
+
# host made work — nothing for us to second-guess.
|
|
93
|
+
return if adopt_existing_table?
|
|
94
|
+
|
|
95
|
+
classes = devise_auth_class_names
|
|
96
|
+
return if classes.empty? # mappings unreadable — stay permissive
|
|
97
|
+
return if classes == ["User"] # the default assumption holds
|
|
98
|
+
|
|
99
|
+
if classes.include?("User")
|
|
100
|
+
# User plus other scopes: default mode tracks User and SILENTLY
|
|
101
|
+
# skips the rest (the runtime adapter's row_accepts? guard) —
|
|
102
|
+
# surface that tradeoff at install time, but proceed.
|
|
103
|
+
say "⚠️ Multiple Devise models detected (#{classes.join(", ")}).", :yellow
|
|
104
|
+
say " The default install tracks only User — sessions for the other models", :yellow
|
|
105
|
+
say " stay untracked. Re-run with --polymorphic to track them all.", :yellow
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
raise Thor::Error, <<~MSG
|
|
110
|
+
❌ Your Devise model is #{classes.join(", ")} — not User. The default install
|
|
111
|
+
assumes a `User` class (`belongs_to :user`, foreign key to `users`, the
|
|
112
|
+
same assumption `rails generate authentication` makes) and would break
|
|
113
|
+
at migrate/runtime.
|
|
114
|
+
|
|
115
|
+
Re-run with the polymorphic owner, which works with any model(s):
|
|
116
|
+
|
|
117
|
+
rails generate sessions:install --polymorphic
|
|
118
|
+
|
|
119
|
+
…then declare `has_sessions` on #{classes.join(" and ")}.
|
|
120
|
+
MSG
|
|
121
|
+
end
|
|
122
|
+
|
|
81
123
|
def create_migration_files
|
|
82
124
|
if adopt_existing_table?
|
|
83
125
|
migration_template "add_sessions_columns.rb.erb",
|
|
@@ -191,6 +233,19 @@ module Sessions
|
|
|
191
233
|
defined?(::Devise) ? true : false
|
|
192
234
|
end
|
|
193
235
|
|
|
236
|
+
# The class names behind the host's `devise_for` scopes, as strings
|
|
237
|
+
# (never constantized — the generator must not boot-order-couple to
|
|
238
|
+
# app models). On Rails 8, `Devise.mappings` itself force-loads the
|
|
239
|
+
# lazy routes (Devise 5.x, lib/devise.rb), so an empty hash genuinely
|
|
240
|
+
# means "no devise_for drawn yet" — nothing to validate against.
|
|
241
|
+
def devise_auth_class_names
|
|
242
|
+
return [] unless devise_detected?
|
|
243
|
+
|
|
244
|
+
::Devise.mappings.values.map { |mapping| mapping.class_name.to_s }.uniq.sort
|
|
245
|
+
rescue StandardError
|
|
246
|
+
[]
|
|
247
|
+
end
|
|
248
|
+
|
|
194
249
|
def adopt_existing_table?
|
|
195
250
|
return @adopt_existing_table if defined?(@adopt_existing_table)
|
|
196
251
|
|
|
@@ -79,16 +79,26 @@ class Create<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_ve
|
|
|
79
79
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
|
-
# Honor the host's configured primary key type (uuid
|
|
83
|
-
# same setting `rails g model` uses, so an app generated with
|
|
82
|
+
# Honor the host's configured primary key type (uuid, string/ULID, bigint…).
|
|
83
|
+
# Reads the same setting `rails g model` uses, so an app generated with
|
|
84
84
|
# `config.generators { |g| g.orm :active_record, primary_key_type: :uuid }`
|
|
85
85
|
# gets uuid sessions tables and uuid foreign keys, automatically.
|
|
86
86
|
def primary_and_foreign_key_types
|
|
87
87
|
config = Rails.configuration.generators
|
|
88
88
|
setting = config.options[config.orm][:primary_key_type]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
[ setting || :primary_key, reference_column_type(setting) ]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Reference columns hold VALUES of the PK type, so the serial pseudo-types
|
|
93
|
+
# map to their plain integer equivalents — a `t.bigserial` reference would
|
|
94
|
+
# mint its own auto-increment sequence, which is exactly wrong for a
|
|
95
|
+
# foreign key. Everything else (uuid, string ULIDs, …) passes through.
|
|
96
|
+
def reference_column_type(setting)
|
|
97
|
+
case setting
|
|
98
|
+
when nil, :bigserial then :bigint
|
|
99
|
+
when :serial then :integer
|
|
100
|
+
else setting
|
|
101
|
+
end
|
|
92
102
|
end
|
|
93
103
|
|
|
94
104
|
# match? (not equality): PostGIS apps report adapter_name "PostGIS" and
|
|
@@ -85,17 +85,26 @@ class CreateSessionsEvents < ActiveRecord::Migration<%= migration_version %>
|
|
|
85
85
|
def primary_and_foreign_key_types
|
|
86
86
|
config = Rails.configuration.generators
|
|
87
87
|
setting = config.options[config.orm][:primary_key_type]
|
|
88
|
-
|
|
89
|
-
foreign_key_type = setting || :bigint
|
|
90
|
-
[ primary_key_type, foreign_key_type ]
|
|
88
|
+
[ setting || :primary_key, reference_column_type(setting) ]
|
|
91
89
|
end
|
|
92
90
|
|
|
93
91
|
# session_id must match the sessions table's PRIMARY KEY type (uuid hosts
|
|
94
|
-
# store uuid linkage
|
|
92
|
+
# store uuid linkage, string/ULID hosts string, bigint hosts bigint).
|
|
95
93
|
def session_id_column_type
|
|
96
94
|
config = Rails.configuration.generators
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
reference_column_type(config.options[config.orm][:primary_key_type])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reference columns hold VALUES of the PK type, so the serial pseudo-types
|
|
99
|
+
# map to their plain integer equivalents — a `t.bigserial` reference would
|
|
100
|
+
# mint its own auto-increment sequence, which is exactly wrong for a
|
|
101
|
+
# foreign key. Everything else (uuid, string ULIDs, …) passes through.
|
|
102
|
+
def reference_column_type(setting)
|
|
103
|
+
case setting
|
|
104
|
+
when nil, :bigserial then :bigint
|
|
105
|
+
when :serial then :integer
|
|
106
|
+
else setting
|
|
107
|
+
end
|
|
99
108
|
end
|
|
100
109
|
|
|
101
110
|
# match? (not equality): PostGIS apps report adapter_name "PostGIS" and
|
|
@@ -36,8 +36,9 @@ module Sessions
|
|
|
36
36
|
SKIP_ENV_KEY = "sessions.skip" # = Sessions::SKIP_ENV_KEY (set by Sessions.skip!)
|
|
37
37
|
|
|
38
38
|
# The `throw :warden` message on revoked sessions — Devise's failure
|
|
39
|
-
# app surfaces it like :timeout/:session_limited
|
|
40
|
-
# `devise.failure.session_revoked`
|
|
39
|
+
# app surfaces it like :timeout/:session_limited. The gem SHIPS the
|
|
40
|
+
# `devise.failure.session_revoked` copy (en + es, config/locales/);
|
|
41
|
+
# hosts override that key for custom wording.
|
|
41
42
|
THROW_MESSAGE = :session_revoked
|
|
42
43
|
|
|
43
44
|
module_function
|
|
@@ -185,11 +186,38 @@ module Sessions
|
|
|
185
186
|
Sessions.safely("warden.adopt") do
|
|
186
187
|
next unless row_accepts?(record)
|
|
187
188
|
|
|
189
|
+
# IDEMPOTENT, because a client that can't persist cookies re-enters
|
|
190
|
+
# adoption on EVERY request: the SESSION_KEY we write rides a
|
|
191
|
+
# Set-Cookie the client drops, so the next request adopts again —
|
|
192
|
+
# unbounded rows (production-found: a native HTTP layer that
|
|
193
|
+
# forwarded cookies read-only minted one adopted row per location
|
|
194
|
+
# ping, hundreds per ride). Same user, same scope, same UA,
|
|
195
|
+
# recently adopted → that's this same client: touch it, mint
|
|
196
|
+
# nothing. No token rotation either — a sibling client sharing the
|
|
197
|
+
# cookie jar (the app's WebView next to its native HTTP stack) may
|
|
198
|
+
# hold a VALID key to this row, and rotating would kick it.
|
|
199
|
+
if (row = recent_adopted_row(record, warden, scope))
|
|
200
|
+
Sessions.safely("warden.adopt.touch") { row.touch_last_seen!(warden.request) }
|
|
201
|
+
next
|
|
202
|
+
end
|
|
203
|
+
|
|
188
204
|
row = create_row_for(record, warden, scope, suppress_login_event: true)
|
|
189
205
|
row&.update_columns(auth_detail: { "adopted" => true })
|
|
190
206
|
end
|
|
191
207
|
end
|
|
192
208
|
|
|
209
|
+
def recent_adopted_row(record, warden, scope)
|
|
210
|
+
model = Sessions.session_model
|
|
211
|
+
rows = model.where(user: record)
|
|
212
|
+
rows = rows.where(scope: scope.to_s) if model.column_names.include?("scope")
|
|
213
|
+
rows = rows.where(user_agent: warden.request.user_agent) if model.column_names.include?("user_agent")
|
|
214
|
+
|
|
215
|
+
rows.where(model.arel_table[:created_at].gt(24.hours.ago))
|
|
216
|
+
.order(created_at: :desc)
|
|
217
|
+
.limit(10)
|
|
218
|
+
.detect { |row| row.try(:auth_detail).to_h["adopted"] }
|
|
219
|
+
end
|
|
220
|
+
|
|
193
221
|
# SCOPE-PRECISE teardown: only this scope's warden entries go (the
|
|
194
222
|
# serialized user key and our token stash) — an admin scope riding
|
|
195
223
|
# the same rack session, and unrelated host session data (carts,
|
|
@@ -225,7 +253,9 @@ module Sessions
|
|
|
225
253
|
credentials = request.params[scope.to_s]
|
|
226
254
|
next unless credentials.is_a?(Hash)
|
|
227
255
|
|
|
228
|
-
|
|
256
|
+
# `email_address` included: it's the omakase-era key, and Devise
|
|
257
|
+
# apps configure `authentication_keys = [:email_address]` too.
|
|
258
|
+
identity = credentials.values_at("email", "email_address", "login", "username", "phone").compact.first
|
|
229
259
|
|
|
230
260
|
Sessions::Event.record_failure(
|
|
231
261
|
request,
|
data/lib/sessions/classifier.rb
CHANGED
|
@@ -153,7 +153,13 @@ module Sessions
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def method_for_strategy(strategy_name)
|
|
156
|
-
|
|
156
|
+
# Host entries are consulted FIRST (Hash#merge keeps the receiver's
|
|
157
|
+
# keys in front, so config substrings win the iteration order) and the
|
|
158
|
+
# block makes them WIN on a shared key too — a bare `merge` would let
|
|
159
|
+
# the built-in value silently clobber a host override of, say,
|
|
160
|
+
# "Rememberable".
|
|
161
|
+
mappings = Sessions.config.strategy_methods.merge(STRATEGY_METHODS) { |_key, custom, _builtin| custom }
|
|
162
|
+
mappings.each do |substring, method|
|
|
157
163
|
return method if strategy_name.include?(substring)
|
|
158
164
|
end
|
|
159
165
|
nil
|
|
@@ -335,7 +335,14 @@ module Sessions
|
|
|
335
335
|
|
|
336
336
|
def sessions_record_login
|
|
337
337
|
Sessions.safely("record_login") do
|
|
338
|
-
|
|
338
|
+
if sessions_suppress_login_event
|
|
339
|
+
# Suppressed writes (adoption) skip the trail event, dedup and
|
|
340
|
+
# the new-device hook — but never the cap: it's the hard limit on
|
|
341
|
+
# LIVE rows, and a misbehaving client looping through adoption
|
|
342
|
+
# must hit it like everyone else.
|
|
343
|
+
sessions_enforce_cap!
|
|
344
|
+
next
|
|
345
|
+
end
|
|
339
346
|
|
|
340
347
|
# Same browser signing in again (abandoned session, expired
|
|
341
348
|
# remember-me, browser update — anything) replaces its old row
|
data/lib/sessions/version.rb
CHANGED