-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Using ActionCable to poll the backend via websockets to determine use…
…r activity and extend or cancel the session
- Loading branch information
1 parent
836acaa
commit c077628
Showing
9 changed files
with
313 additions
and
233 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,13 @@ | ||
module ApplicationCable | ||
class Connection < ActionCable::Connection::Base | ||
identified_by :current_user, :session_id | ||
identified_by :session_id | ||
|
||
def connect | ||
self.current_user = find_verified_user | ||
self.session_id = session.id | ||
logger.add_tags "ActionCable", current_user&.id || "Guest-#{session_id}" | ||
end | ||
self.current_user = find_verified_user | ||
|
||
def session | ||
@request.session | ||
end | ||
|
||
private | ||
|
||
def find_verified_user | ||
if verified_user = env['warden'].user | ||
verified_user | ||
else | ||
nil | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,88 +1,169 @@ | ||
class SessionChannel < ApplicationCable::Channel | ||
# Check session activity every minute | ||
periodically :check_session_activity, every: 60.seconds | ||
# Check session activity every minute in production, more frequently in development | ||
periodically :check_session_activity, every: Rails.env.development? ? 5.seconds : 60.seconds | ||
|
||
def subscribed | ||
identifier = current_user || connection.session[:cbv_flow_id] || connection.session.id | ||
stream_for identifier | ||
@cbv_flow_id = connection.session[:cbv_flow_id] | ||
stream_for current_channel_identifier | ||
|
||
# Record connection time | ||
connection.instance_variable_set(:@session_channel_connected_at, Time.current) | ||
|
||
# Send session info to client | ||
|
||
# Track this initial connection as an activity | ||
update_last_activity_time | ||
|
||
# Send initial session info | ||
transmit_session_info | ||
end | ||
|
||
def received(data) | ||
# This is called whenever data is received from the client | ||
# We can use this to track user activity at the websocket level | ||
Rails.logger.debug "Data received on SessionChannel: #{data}" | ||
# This method is called for general messages received from the client | ||
# that don't match a specific method name in perform('method_name'). | ||
# ActionCable automatically routes messages to methods named in 'perform'. | ||
# Example: Client calls subscription.perform('extend_session') will directly | ||
# call the extend_session method, bypassing this received method. | ||
|
||
Rails.logger.debug "Data received on SessionChannel: #{data}" | ||
|
||
# Update last activity time | ||
connection.instance_variable_set(:@session_channel_last_activity, Time.current) | ||
update_last_activity_time | ||
end | ||
|
||
# Send current session information to the client | ||
def transmit_session_info | ||
identifier = current_user || connection.session[:cbv_flow_id] || connection.session.id | ||
|
||
if current_user && current_user.respond_to?(:last_request_at) && current_user.last_request_at | ||
# For authenticated users, use Devise timeout (default 30 minutes) | ||
timeout_in = defined?(Devise) && Devise.respond_to?(:timeout_in) ? Devise.timeout_in : 30.minutes | ||
last_active = current_user.last_request_at || Time.current | ||
time_until_timeout = [timeout_in - (Time.current - last_active), 0].max | ||
|
||
transmit({ | ||
event: "session.info", | ||
timeout_in_ms: timeout_in * 1000, | ||
time_remaining_ms: time_until_timeout * 1000, | ||
last_activity: last_active.iso8601 | ||
}) | ||
elsif connection.session[:cbv_flow_id].present? | ||
# For CBV flow, use 30 minute timeout | ||
transmit({ | ||
event: "session.info", | ||
timeout_in_ms: 30.minutes * 1000, | ||
time_remaining_ms: 30.minutes * 1000, | ||
last_activity: Time.current.iso8601 | ||
}) | ||
def extend_session(data) | ||
update_last_activity_time | ||
|
||
if @cbv_flow_id | ||
connection.session[:last_activity_at] = Time.current.to_i | ||
Rails.logger.debug "Updated session timestamp for CBV flow #{@cbv_flow_id}" | ||
end | ||
|
||
# Clear any warning flags since the session has been extended | ||
connection.instance_variable_set(:@warning_sent_at, nil) | ||
|
||
Rails.logger.info "Transmitting session.extended event" | ||
transmit({ | ||
event: "session.extended", | ||
message: "Session extended successfully", | ||
extended_at: Time.current.iso8601 | ||
}) | ||
|
||
# Also update session info with new timestamps | ||
transmit_session_info | ||
|
||
Rails.logger.info "Session successfully extended for #{current_channel_identifier}" | ||
end | ||
|
||
def transmit_session_info | ||
# Get the Devise timeout setting | ||
timeout_in = get_timeout_setting | ||
|
||
# Calculate time remaining | ||
last_active = get_last_activity_time | ||
time_until_timeout = [ timeout_in - (Time.current - last_active), 0 ].max | ||
|
||
transmit({ | ||
event: "session.info", | ||
timeout_in_ms: timeout_in * 1000, | ||
time_remaining_ms: time_until_timeout * 1000, | ||
last_activity: last_active.iso8601 | ||
}) | ||
end | ||
|
||
private | ||
|
||
def check_session_activity | ||
identifier = current_user || connection.session[:cbv_flow_id] || connection.session.id | ||
return unless identifier | ||
|
||
# Get last activity time either from the connection or from the user's last_request_at | ||
last_activity = connection.instance_variable_get(:@session_channel_last_activity) | ||
def current_channel_identifier | ||
current_user&.id || @cbv_flow_id || connection.session.id | ||
end | ||
|
||
def get_timeout_setting | ||
# In development, use a shorter timeout for testing (2 minutes) | ||
Rails.env.development? ? 2.minutes : 30.minutes | ||
end | ||
|
||
# Update the last activity timestamp | ||
def update_last_activity_time | ||
current_time = Time.current | ||
connection.instance_variable_set(:@session_channel_last_activity, current_time) | ||
|
||
if current_user && current_user.respond_to?(:last_request_at) && current_user.last_request_at | ||
# For authenticated users, use Devise timeout (default 30 minutes) | ||
timeout_in = defined?(Devise) && Devise.respond_to?(:timeout_in) ? Devise.timeout_in : 30.minutes | ||
last_active = [last_activity, current_user.last_request_at].compact.max || Time.current | ||
time_until_timeout = [timeout_in - (Time.current - last_active), 0].max | ||
Rails.logger.debug "User #{current_user.id} session expires in #{time_until_timeout} seconds" | ||
else | ||
# For CBV flow, use session timeout (30 minutes) | ||
timeout_in = 30.minutes | ||
connection_time = connection.instance_variable_get(:@session_channel_connected_at) || Time.current | ||
last_active = last_activity || connection_time | ||
time_until_timeout = [timeout_in - (Time.current - last_active), 0].max | ||
# Also update the session timestamp | ||
if connection.session | ||
connection.session[:last_activity_at] = current_time.to_i | ||
end | ||
|
||
Rails.logger.debug "Updated activity time for #{current_channel_identifier} to #{current_time}" | ||
end | ||
|
||
def get_last_activity_time | ||
# Get timestamps from different sources | ||
websocket_activity = connection.instance_variable_get(:@session_channel_last_activity) | ||
connection_time = connection.instance_variable_get(:@session_channel_connected_at) | ||
# Get activity time from the session (if tracked in ApplicationController) | ||
session_activity = connection.session && connection.session[:last_activity_at].present? ? | ||
Time.at(connection.session[:last_activity_at]) : nil | ||
|
||
# Log the available timestamps for debugging | ||
timestamps = { | ||
websocket_activity: websocket_activity, | ||
session_activity: session_activity, | ||
connection_time: connection_time | ||
} | ||
Rails.logger.debug "Available timestamps for #{current_channel_identifier}: #{timestamps.inspect}" | ||
|
||
# Use the most recent timestamp from any source, fallback to current time | ||
[ websocket_activity, session_activity, connection_time, Time.current ].compact.max | ||
end | ||
|
||
# Convert to milliseconds for JavaScript | ||
def check_session_activity | ||
Rails.logger.debug "SessionChannel#check_session_activity called at #{Time.current}" | ||
return unless @cbv_flow_id | ||
|
||
# Get the timeout setting | ||
session_timeout_limit = get_timeout_setting | ||
|
||
# Get the most recent activity timestamp | ||
last_active = get_last_activity_time | ||
|
||
# Calculate time until timeout | ||
time_until_timeout = [ session_timeout_limit - (Time.current - last_active), 0 ].max | ||
time_until_timeout_ms = time_until_timeout * 1000 | ||
|
||
# Log detailed information for debugging | ||
Rails.logger.debug "Session for #{current_channel_identifier}: timeout_limit=#{session_timeout_limit}, last_active=#{last_active}, " + | ||
"current=#{Time.current}, time_until_timeout=#{time_until_timeout}s" | ||
|
||
# Send warning when approaching timeout (5 minutes before) | ||
warning_threshold = 5.minutes.to_i * 1000 | ||
if time_until_timeout_ms <= warning_threshold && time_until_timeout_ms > 0 | ||
broadcast_to(identifier, { | ||
warning_already_sent = connection.instance_variable_get(:@warning_sent_at) | ||
|
||
# Only send a warning if we're in the warning window AND | ||
# (we haven't sent a warning yet OR the previous warning was sent more than 1 minute ago) | ||
if time_until_timeout_ms <= warning_threshold && time_until_timeout_ms > 0 && | ||
(warning_already_sent.nil? || Time.current - warning_already_sent > 1.minute) | ||
|
||
# Record that we sent a warning | ||
connection.instance_variable_set(:@warning_sent_at, Time.current) | ||
|
||
Rails.logger.info "Sending session timeout warning to #{current_channel_identifier}, #{time_until_timeout_ms / 1000} seconds remaining" | ||
|
||
broadcast_to(current_channel_identifier, { | ||
event: "session.warning", | ||
message: "Your session will expire soon", | ||
time_remaining_ms: time_until_timeout_ms | ||
}) | ||
end | ||
|
||
# Debug info in development | ||
if Rails.env.development? | ||
broadcast_to(current_channel_identifier, { | ||
event: "session.debug", | ||
message: "Session activity check ran", | ||
time_remaining_ms: time_until_timeout_ms, | ||
last_activity: last_active.iso8601, | ||
timeout_in: session_timeout_limit.to_i, | ||
current_time: Time.current.iso8601, | ||
cbv_flow_id: @cbv_flow_id, | ||
warning_sent_at: connection.instance_variable_get(:@warning_sent_at)&.iso8601 | ||
}) | ||
end | ||
end | ||
end | ||
end |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.