session-check 1.1.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c086b705a832734d76b4abdeb2e2ce98c3b3278629efce7a7dd0714cedf0134
4
- data.tar.gz: 4059cbfc502675d24fbb5360e4c3e9de0917690165aaab8a1a39f2ea51f0c33d
3
+ metadata.gz: 75caa11e24f6f750c18ee096a9d39ba72c86713e74836d479885625f2d72d262
4
+ data.tar.gz: 6eaa656c8f9aac3a935483393aa4835e9286d6e96fe87446477c238ab575cd75
5
5
  SHA512:
6
- metadata.gz: f330839b1cee46fb05e160c0c5f3174a38611a8711a113533a41102016f2cdc6b833787fc95835a33cc07f6dd3bb36e2bd895066b905d429df68428ec48c8106
7
- data.tar.gz: c0f2cac5d5f86949e0a6c924d35badafdc2bab9c9ae435eae1f5990745476a1d8be383b7ff24f82c623f35ca9a4cc9e2ed3d00b476c4f0e1f575bc1c71548722
6
+ metadata.gz: 5375698f650fb0fccf732a4a7c735e4e9248084a6b3d2f3126d49be384116cfb3b66751bbc549f137c6f6f3971a99d35dc2fbc641ce611136aff0718f073a67e
7
+ data.tar.gz: 9bc95912cb792257bd9449702a0092d29331b5be003b191061b2b7b8b3e62add21d1bef242513ac7f0b8409ef5a6db2bfc2752bd099bc25a04149b6d983b33bc
data/README.md CHANGED
@@ -1,4 +1,8 @@
1
- # Session Check
1
+ Session Check
2
+ =========
3
+
4
+ [![Gem Version](https://img.shields.io/gem/v/session-check?color=green)](https://rubygems.org/gems/session-check)
5
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
2
6
 
3
7
  A gem that returns you to your application's sign in page when your Devise session expires.
4
8
 
@@ -29,7 +33,65 @@ their browser, you can start the ping process by calling:
29
33
 
30
34
  SessionCheck.should_session_check = true;
31
35
 
36
+ # Configuration
37
+
38
+ `logged_out_url` — the URL users are redirected to when their session expires. Defaults to `/users/sign_in`.
39
+
40
+ Session::Check.configure do |config|
41
+ config.logged_out_url = '/login'
42
+ config.check_every_s = 30
43
+ end
44
+
45
+ These can also be overridden per-call:
46
+
47
+ <%= session_check logged_out_url: '/login', check_every_s: 30 %>
48
+
49
+ # Custom session detection
50
+
51
+ If your application uses a non-Devise session mechanism (e.g. token-based principals stored in the session hash),
52
+ you can override how the gem detects an active session by configuring a `session_active_proc`.
53
+
54
+ The proc receives the current context (controller in the ping endpoint, view/helper context in the `session_check` helper) and must return a Hash with two keys:
55
+ - `exists` — Boolean, whether an active session is present
56
+ - `expires_in` — Integer (seconds), how long until the session expires
57
+
58
+ Session::Check.configure do |config|
59
+ config.session_active_proc = ->(controller) {
60
+ if controller.current_user
61
+ # Devise-backed session
62
+ expires_in = Session::Check::Devise.expires_in(controller.session)
63
+ { exists: true, expires_in: expires_in }
64
+ elsif controller.session[:my_custom_principal]
65
+ { exists: true, expires_in: 3600 }
66
+ else
67
+ { exists: false, expires_in: 0 }
68
+ end
69
+ }
70
+ end
71
+
72
+ When `session_active_proc` is configured it replaces the default `current_user` check in both the
73
+ server-side ping endpoint and the initial JS `should_session_check` value. All other behaviour
74
+ (check interval, AJAX counter reset, redirect URL) remains unchanged and configurable via the
75
+ `session_check` helper options.
76
+
77
+ If `session_active_proc` is not set the gem uses the default Devise behaviour, computing remaining session time from the Warden last-request timestamp.
78
+
79
+ # Development
80
+
81
+ The client-side JS lives in `app/assets/javascripts/session_check.js` as plain, dependency-free JS
82
+ so it can be unit tested in isolation. The `_session_check` partial only passes configuration in via
83
+ `window.SessionCheckConfig` and inlines this file's contents — no asset pipeline setup is required
84
+ by consuming applications.
85
+
86
+ JS unit tests (Jest + jsdom) live under `spec/javascript` and are scoped to this repo only —
87
+ `package.json`/`node_modules` are dev-only and excluded from the published gem. Run them with:
88
+
89
+ npm install
90
+ npm test
91
+
32
92
  # Changelog
33
93
 
94
+ Version 2.0.1 : Fix session-expiry check drifting in background/inactive tabs
95
+ Version 2.0.0 : **Breaking change** — default Devise behaviour now correctly computes remaining session time from the Warden last-request timestamp rather than always returning the full timeout. `current_user` is no longer exposed to the session check partial. Added `session_active_proc` configuration option for non-Devise session support. Fixed setTimeout multiplier (5000 → 1000) so session checks fire at the correct interval. Bump your dependency to `>= 2.0.0`.
34
96
  Version 1.1.0 : Added optional nonce
35
97
  Version 0.2.1 : Added explicit reference to Devise (which is required)
@@ -0,0 +1,125 @@
1
+ /* eslint-disable no-var */
2
+ // This file is plain JS (no ERB) so it can be loaded directly in unit tests
3
+ // (see spec/javascript) as well as served as-is by the Rails asset pipeline.
4
+ //
5
+ // Configuration is supplied at runtime via a `window.SessionCheckConfig`
6
+ // object assigned by app/views/_session_check.html.erb, e.g.:
7
+ //
8
+ // window.SessionCheckConfig = {
9
+ // should_session_check: true,
10
+ // check_every_s: 30,
11
+ // session_time: 3600,
12
+ // logged_out_url: '/users/sign_in',
13
+ // reset_counter_on_ajax: true
14
+ // };
15
+ (function (global) {
16
+ 'use strict';
17
+
18
+ // `createSessionCheck` builds a fresh instance of the session-check
19
+ // behaviour. `deps` allows tests to inject fakes for jQuery, the clock and
20
+ // timer functions instead of relying on browser globals.
21
+ function createSessionCheck(config, deps) {
22
+ config = config || {};
23
+ deps = deps || {};
24
+
25
+ var $ = deps.$ || global.$;
26
+ var now = deps.now || function () {
27
+ return Date.now();
28
+ };
29
+ var setTimeoutFn = deps.setTimeout || global.setTimeout;
30
+ var documentRef = deps.document || global.document;
31
+ var locationRef = deps.location || global;
32
+
33
+ var check_every_s = config.check_every_s;
34
+ var session_time = config.session_time;
35
+ var check_every_ms = check_every_s * 1000;
36
+ var min_check_interval_ms = 5 * 1000; // don't re-check more than once per 5s
37
+ var session_expires_at = now() + session_time * 1000; // absolute timestamp, not a countdown
38
+ var last_check_at = now();
39
+
40
+ var SessionCheck = {
41
+ should_session_check: !!config.should_session_check
42
+ };
43
+
44
+ var force_sign_in = function () {
45
+ locationRef.location = config.logged_out_url;
46
+ };
47
+
48
+ var check_session_with_server = function () {
49
+ last_check_at = now();
50
+ $.get('/session_check/time_to_session_expiry')
51
+ .done(function (d) {
52
+ if (!d.session_exists) {
53
+ force_sign_in();
54
+ } else {
55
+ session_expires_at = now() + d.session_expires_in * 1000;
56
+ }
57
+ })
58
+ .fail(force_sign_in);
59
+ };
60
+
61
+ var session_check = function () {
62
+ if (SessionCheck.should_session_check && now() >= session_expires_at) {
63
+ check_session_with_server();
64
+ }
65
+ setTimeoutFn(session_check, check_every_ms);
66
+ };
67
+
68
+ var on_visibility_change = function () {
69
+ if (documentRef.visibilityState === 'visible' && SessionCheck.should_session_check) {
70
+ if (now() - last_check_at > min_check_interval_ms) {
71
+ check_session_with_server();
72
+ }
73
+ }
74
+ };
75
+
76
+ var on_ajax_complete = function () {
77
+ session_expires_at = now() + session_time * 1000;
78
+ last_check_at = now();
79
+ };
80
+
81
+ var start = function () {
82
+ setTimeoutFn(session_check, check_every_ms);
83
+
84
+ // Whenever the tab becomes visible again, go straight to the server
85
+ // for ground truth instead of trusting local timers/clock math.
86
+ // Debounced so rapid tab-switching doesn't spam the endpoint.
87
+ documentRef.addEventListener('visibilitychange', on_visibility_change);
88
+
89
+ if (config.reset_counter_on_ajax) {
90
+ $.ajaxSetup({
91
+ complete: on_ajax_complete
92
+ });
93
+ }
94
+ };
95
+
96
+ return {
97
+ SessionCheck: SessionCheck,
98
+ start: start,
99
+ // Exposed for unit testing only.
100
+ _internal: {
101
+ force_sign_in: force_sign_in,
102
+ check_session_with_server: check_session_with_server,
103
+ session_check: session_check,
104
+ on_visibility_change: on_visibility_change,
105
+ on_ajax_complete: on_ajax_complete,
106
+ get session_expires_at() {
107
+ return session_expires_at;
108
+ },
109
+ get last_check_at() {
110
+ return last_check_at;
111
+ }
112
+ }
113
+ };
114
+ }
115
+
116
+ if (typeof module !== 'undefined' && module.exports) {
117
+ module.exports = createSessionCheck;
118
+ }
119
+
120
+ if (typeof global.SessionCheckConfig !== 'undefined') {
121
+ var instance = createSessionCheck(global.SessionCheckConfig);
122
+ global.SessionCheck = instance.SessionCheck;
123
+ instance.start();
124
+ }
125
+ })(typeof window !== 'undefined' ? window : this);
@@ -7,38 +7,15 @@ module Session
7
7
 
8
8
  prepend_before_action :dont_update_request_time
9
9
 
10
- # Find it there is a session, and if it has any warden information. If so, the user is logged in.
11
10
  def time_to_session_expiry
12
- session_exists = false
13
- session_expires_in = 0
14
- if current_user
15
- session_exists = true
16
- # This calculates how many seconds there are until they are logged out
17
- session_expires_in = calculate_session_expires_in
18
- end
19
- render json: { session_exists: session_exists, session_expires_in: session_expires_in }
11
+ result = Session::Check.configuration.call_session_active_proc(self)
12
+ render json: { session_exists: !!result[:exists], session_expires_in: result[:expires_in].to_i }
20
13
  end
21
14
 
22
15
  # This ensures this request ping doesn't update their last access time.
23
16
  private def dont_update_request_time
24
17
  request.env['devise.skip_trackable'] = true
25
18
  end
26
-
27
- private def calculate_session_expires_in
28
- User.timeout_in -
29
- Time.now.utc.to_i.seconds +
30
- SessionChecksController.time_of_last_warden_request(session).to_i.seconds
31
- rescue => _e
32
- 1000000.seconds
33
- end
34
-
35
- class << self
36
- def time_of_last_warden_request(session)
37
- session['warden.user.user.session']['last_request_at']
38
- rescue => _e
39
- Time.zone.now
40
- end
41
- end
42
19
  end
43
20
  end
44
21
  end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'devise'
4
-
5
3
  module Session
6
4
  module Check
7
5
  module SessionCheckHelper
8
6
  def session_check(options = {})
7
+ result = Session::Check.configuration.call_session_active_proc(self)
8
+ session_active = !!result[:exists]
9
+ session_time = result[:expires_in].to_i
10
+
9
11
  locals = {
10
- session_time: Devise.timeout_in,
11
- check_every: 10,
12
+ session_time: session_time,
13
+ check_every_s: Session::Check.configuration.check_every_s,
12
14
  reset_counter_on_ajax: true,
13
- logged_out_url: '/users/sign_in',
14
- current_user: current_user
15
- }.merge options
15
+ logged_out_url: Session::Check.configuration.logged_out_url,
16
+ session_active: session_active
17
+ }.merge(options)
16
18
 
17
19
  ActionController::Base.render(partial: '/session_check', locals: locals)
18
20
  end
@@ -1,35 +1,12 @@
1
1
  <script<% if local_assigns[:nonce] %> nonce="<%= nonce %>"<% end %>>
2
- var SessionCheck = {
3
- should_session_check: <%= !current_user.nil? %>
4
- };
5
- (function () {
6
- var check_every_s = <%= check_every %>;
7
- var session_time_left = <%= session_time %>;
8
- var force_sign_in = function () {
9
- window.location = '<%= logged_out_url %>';
10
- };
11
- var session_check = function () {
12
- session_time_left = session_time_left - check_every_s;
13
- if (SessionCheck.should_session_check && session_time_left < 0) {
14
- $.get('/session_check/time_to_session_expiry')
15
- .done(function (d) {
16
- if (!d.session_exists) {
17
- force_sign_in();
18
- } else {
19
- session_time_left = d.session_expires_in;
20
- }
21
- })
22
- .fail(force_sign_in);
23
- }
24
- setTimeout(session_check, check_every_s * 5000);
25
- };
26
- setTimeout(session_check, check_every_s * 5000);
27
- <% if reset_counter_on_ajax %>
28
- $.ajaxSetup({
29
- complete: function (xhr) {
30
- session_time_left = <%= session_time %>;
31
- }
32
- });
33
- <% end %>
34
- }());
35
- </script>
2
+ window.SessionCheckConfig = {
3
+ should_session_check: <%= session_active %>,
4
+ check_every_s: <%= check_every_s %>,
5
+ session_time: <%= session_time %>,
6
+ logged_out_url: <%= raw ERB::Util.json_escape(logged_out_url.to_json) %>,
7
+ reset_counter_on_ajax: <%= !!reset_counter_on_ajax %>
8
+ };
9
+ </script>
10
+ <script<% if local_assigns[:nonce] %> nonce="<%= nonce %>"<% end %>>
11
+ <%= raw File.read(Session::Check::Engine.root.join('app', 'assets', 'javascripts', 'session_check.js')) %>
12
+ </script>
data/config/routes.rb CHANGED
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Draw directly into the host application's routes (rather than
4
+ # `Session::Check::Engine.routes.draw`) so the endpoint is auto-wired for any
5
+ # consuming app without requiring an explicit `mount Session::Check::Engine`,
6
+ # matching the README usage (`<%= session_check %>` only).
3
7
  Rails.application.routes.draw do
4
- get 'session_check/time_to_session_expiry', to: 'session/check/session_checks#time_to_session_expiry'
8
+ get 'session_check/time_to_session_expiry',
9
+ to: 'session/check/session_checks#time_to_session_expiry',
10
+ format: :json,
11
+ defaults: { format: :json }
5
12
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'devise'
4
+
5
+ module Session
6
+ module Check
7
+ class Configuration
8
+ # Proc used to determine session state. Called with the controller/helper context as its
9
+ # sole argument. Must return a Hash: { exists: Boolean, expires_in: Integer (seconds) }
10
+ #
11
+ # Override to customise session detection for non-Devise sessions.
12
+ attr_accessor :session_active_proc, :logged_out_url, :check_every_s
13
+
14
+ def initialize
15
+ @logged_out_url = '/users/sign_in'
16
+ @check_every_s = 10
17
+ @session_active_proc = ->(controller) {
18
+ user = begin
19
+ controller.current_user
20
+ rescue NoMethodError
21
+ nil
22
+ end
23
+ if user
24
+ expires_in = Session::Check::Devise.expires_in(controller.session)
25
+ { exists: true, expires_in: expires_in }
26
+ else
27
+ { exists: false, expires_in: 0 }
28
+ end
29
+ }
30
+ end
31
+
32
+ def call_session_active_proc(context)
33
+ result = session_active_proc.call(context)
34
+ unless result.is_a?(Hash)
35
+ raise ArgumentError,
36
+ "session_active_proc must return a Hash with :exists and :expires_in keys, got #{result.class}"
37
+ end
38
+ normalized = result.transform_keys(&:to_sym)
39
+ { exists: normalized[:exists], expires_in: normalized[:expires_in] }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Session
4
+ module Check
5
+ module Devise
6
+ def self.expires_in(session)
7
+ last_request_at = begin
8
+ session['warden.user.user.session']['last_request_at'].to_i
9
+ rescue NoMethodError, TypeError
10
+ Time.now.utc.to_i
11
+ end
12
+ remaining = ::Devise.timeout_in.to_i - (Time.now.utc.to_i - last_request_at)
13
+ [remaining, 0].max
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Session
4
4
  module Check
5
- VERSION = '1.1.0'
5
+ VERSION = '2.0.1'
6
6
  end
7
7
  end
data/lib/session/check.rb CHANGED
@@ -1 +1,17 @@
1
- require_relative 'check/engine'
1
+ require_relative 'check/configuration'
2
+ require_relative 'check/devise'
3
+ require_relative 'check/engine'
4
+
5
+ module Session
6
+ module Check
7
+ class << self
8
+ def configure
9
+ yield configuration
10
+ end
11
+
12
+ def configuration
13
+ @configuration ||= Configuration.new
14
+ end
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: session-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Lascelles
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '5.0'
74
+ version: '7.0'
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '5.0'
81
+ version: '7.0'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: rspec
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -128,12 +128,15 @@ extensions: []
128
128
  extra_rdoc_files: []
129
129
  files:
130
130
  - README.md
131
+ - app/assets/javascripts/session_check.js
131
132
  - app/controllers/session/check/session_checks_controller.rb
132
133
  - app/helpers/session/check/session_check_helper.rb
133
134
  - app/views/_session_check.html.erb
134
135
  - config/routes.rb
135
136
  - lib/session-check.rb
136
137
  - lib/session/check.rb
138
+ - lib/session/check/configuration.rb
139
+ - lib/session/check/devise.rb
137
140
  - lib/session/check/engine.rb
138
141
  - lib/session/check/version.rb
139
142
  homepage: https://github.com/bambooengineering/session-check