diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 5f7ce8f4..48e0dbfa 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -472,6 +472,8 @@ impl RpcHandler { "system.disk-cleanup" => self.handle_system_disk_cleanup().await, "system.reboot" => self.handle_system_reboot(params).await, "system.factory-reset" => self.handle_system_factory_reset(params).await, + "auth.session-policy.get" => self.handle_session_policy_get().await, + "auth.session-policy.set" => self.handle_session_policy_set(params).await, "system.settings.get" => self.handle_system_settings_get(params).await, "system.settings.set" => self.handle_system_settings_set(params).await, "system.kiosk-display.get" => self.handle_system_kiosk_display_get().await, diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 30ed79da..ae05a17f 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -1011,6 +1011,59 @@ impl RpcHandler { } } + /// auth.session-policy.get — how long a login lasts on this node. + pub(in crate::api::rpc) async fn handle_session_policy_get(&self) -> Result { + let policy = crate::settings::session_policy::load(&self.config.data_dir).await; + Ok(serde_json::json!({ + "idle_timeout_secs": policy.idle_timeout_secs, + "absolute_timeout_secs": policy.absolute_timeout_secs, + "reauth_for_funds": policy.reauth_for_funds, + })) + } + + /// auth.session-policy.set — change it. + /// + /// Values are clamped rather than rejected: the caller learns what was + /// actually stored from the reply, which is friendlier than an error and + /// makes the bounds discoverable. Fields are individually optional so the + /// UI can change one control without having to send the others back. + pub(in crate::api::rpc) async fn handle_session_policy_set( + &self, + params: Option, + ) -> Result { + let params = params.unwrap_or(serde_json::json!({})); + let current = crate::settings::session_policy::load(&self.config.data_dir).await; + let policy = crate::settings::session_policy::SessionPolicy { + idle_timeout_secs: params + .get("idle_timeout_secs") + .and_then(|v| v.as_u64()) + .unwrap_or(current.idle_timeout_secs), + absolute_timeout_secs: match params.get("absolute_timeout_secs") { + // Explicit null means "no absolute cap", which is different + // from the field being absent (leave it as it is). + Some(serde_json::Value::Null) => None, + Some(v) => v.as_u64().or(current.absolute_timeout_secs), + None => current.absolute_timeout_secs, + }, + reauth_for_funds: params + .get("reauth_for_funds") + .and_then(|v| v.as_bool()) + .unwrap_or(current.reauth_for_funds), + }; + let saved = crate::settings::session_policy::save(&self.config.data_dir, policy).await?; + tracing::info!( + idle = saved.idle_timeout_secs, + absolute = ?saved.absolute_timeout_secs, + reauth_for_funds = saved.reauth_for_funds, + "session policy updated" + ); + Ok(serde_json::json!({ + "idle_timeout_secs": saved.idle_timeout_secs, + "absolute_timeout_secs": saved.absolute_timeout_secs, + "reauth_for_funds": saved.reauth_for_funds, + })) + } + /// system.settings.set — Write a settings value pub(in crate::api::rpc) async fn handle_system_settings_set( &self, diff --git a/neode-ui/src/views/settings/AccountSection.vue b/neode-ui/src/views/settings/AccountSection.vue index cfe59d4e..5f888144 100644 --- a/neode-ui/src/views/settings/AccountSection.vue +++ b/neode-ui/src/views/settings/AccountSection.vue @@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/app' import AccountInfoSection from '@/views/settings/AccountInfoSection.vue' import ChangePasswordSection from '@/views/settings/ChangePasswordSection.vue' import TwoFactorSection from '@/views/settings/TwoFactorSection.vue' +import SessionTimeoutSection from '@/views/settings/SessionTimeoutSection.vue' const router = useRouter() const { t } = useI18n() @@ -24,6 +25,7 @@ async function handleLogout() { +