From f17797beccc0de68dd81944433ddbe4bb71a55bc Mon Sep 17 00:00:00 2001 From: hz Date: Wed, 22 Jul 2026 22:52:38 -0500 Subject: [PATCH] Don't show the session-expiry warning to actively-used sessions The warning was purely a token-expiry countdown with no concept of user activity, so it interrupted people who had been clicking through questions the whole time. AuthState now tracks real input activity globally and silently refreshes the session in the background when the user has interacted recently, only surfacing the "Stay logged in?" dialog for sessions that are genuinely idle. A failed background refresh still surfaces the dialog rather than silently logging the user out. --- scripts/autoload/auth_state.gd | 13 +++++++++++++ scripts/ui/session_expiry_dialog.gd | 4 ++-- tests/auth_session_test.gd | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/scripts/autoload/auth_state.gd b/scripts/autoload/auth_state.gd index 0e99b6f..e4b12ad 100644 --- a/scripts/autoload/auth_state.gd +++ b/scripts/autoload/auth_state.gd @@ -12,6 +12,7 @@ const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_AUTH_BASE_URL := "https://us3q.ranaze.com/api/Auth" const WARNING_SECONDS := 120 const REFRESH_TIMEOUT_SECONDS := 20.0 +const ACTIVE_RECENTLY_SECONDS := 60 var is_logged_in := false var username := "" @@ -32,6 +33,7 @@ var expiry_timer: Timer var refresh_request: HTTPRequest var session_revision := 0 var refresh_revision := -1 +var last_activity_unix := 0.0 func _ready() -> void: @@ -46,6 +48,12 @@ func _ready() -> void: refresh_request.timeout = REFRESH_TIMEOUT_SECONDS refresh_request.request_completed.connect(_on_refresh_completed) add_child(refresh_request) + last_activity_unix = Time.get_unix_time_from_system() + + +func _input(_event: InputEvent) -> void: + last_activity_unix = Time.get_unix_time_from_system() + func set_session(new_username: String, token: String, new_refresh_token: String = "", new_role: String = "", token_expiration: String = "", legal: Dictionary = {}) -> void: session_revision += 1 @@ -59,6 +67,7 @@ func set_session(new_username: String, token: String, new_refresh_token: String refresh_in_progress = false refresh_revision = -1 warning_active = false + last_activity_unix = Time.get_unix_time_from_system() set_legal_state(legal, false) if is_logged_in and expiration_unix > 0.0: expiry_timer.start() @@ -151,6 +160,10 @@ func _evaluate_expiry() -> void: expire_session("expired") return if remaining <= WARNING_SECONDS: + var seconds_since_activity := Time.get_unix_time_from_system() - last_activity_unix + if seconds_since_activity <= ACTIVE_RECENTLY_SECONDS and not refresh_in_progress: + request_session_refresh() + return if not warning_active: warning_active = true session_warning.emit(remaining) diff --git a/scripts/ui/session_expiry_dialog.gd b/scripts/ui/session_expiry_dialog.gd index a81987b..a93826a 100644 --- a/scripts/ui/session_expiry_dialog.gd +++ b/scripts/ui/session_expiry_dialog.gd @@ -117,8 +117,8 @@ func _refresh_succeeded() -> void: func _refresh_failed(message: String, terminal: bool) -> void: - if not visible: - return + visible = true + move_to_front() error_label.text = message stay_button.text = "Stay logged in" stay_button.disabled = terminal diff --git a/tests/auth_session_test.gd b/tests/auth_session_test.gd index f58931d..6349df7 100644 --- a/tests/auth_session_test.gd +++ b/tests/auth_session_test.gd @@ -14,6 +14,7 @@ func _run() -> void: await process_frame _test_expiration_parsing() _test_warning_and_expiration() + _test_active_user_silently_refreshes_instead_of_warning() _test_refresh_preserves_identity() _test_refresh_in_flight_has_expiry_grace() _test_stale_responses_cannot_restore_or_end_session() @@ -40,6 +41,7 @@ func _test_warning_and_expiration() -> void: auth.session_warning.connect(warning_callback) auth.session_expired.connect(expiration_callback) auth.set_session("tester", "warning-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 90.0)) + auth.last_activity_unix = Time.get_unix_time_from_system() - 3600.0 auth._evaluate_expiry() _assert(int(observed["warning_seconds"]) > 0 and int(observed["warning_seconds"]) <= 90, "session should warn inside the two-minute renewal window") auth.expiration_unix = Time.get_unix_time_from_system() - 1.0 @@ -49,6 +51,25 @@ func _test_warning_and_expiration() -> void: auth.session_expired.disconnect(expiration_callback) +func _test_active_user_silently_refreshes_instead_of_warning() -> void: + var observed := {"warning_seconds": -1} + var warning_callback := func(seconds: int): observed["warning_seconds"] = seconds + auth.session_warning.connect(warning_callback) + # Point at a local, always-refused port so the real refresh attempt this triggers fails + # fast and never touches the real network - the point of this test is that + # request_session_refresh() gets called at all, not that it succeeds. + auth.auth_base_url = "http://127.0.0.1:1" + auth.set_session("tester", "active-token", "refresh-token", "USER", str(Time.get_unix_time_from_system() + 90.0)) + auth.last_activity_unix = Time.get_unix_time_from_system() + auth._evaluate_expiry() + _assert(int(observed["warning_seconds"]) == -1 and not auth.warning_active, "an actively-used session should not show the expiry warning") + _assert(auth.refresh_in_progress, "an actively-used session nearing expiry should silently attempt a refresh instead of warning") + auth.session_warning.disconnect(warning_callback) + auth.refresh_in_progress = false + auth.auth_base_url = auth.DEFAULT_AUTH_BASE_URL + auth.clear_session() + + func _test_refresh_preserves_identity() -> void: var observed := {"refreshed": false} var refreshed_callback := func(): observed["refreshed"] = true