From 141334f7e2a049a67ceccab188ab7330e7725de1 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Wed, 31 Jan 2024 13:14:08 -0800 Subject: [PATCH] add idle timeout and version on settings screen --- src-tauri/Cargo.lock | 27 ++++++++++++++++++--------- src-tauri/Cargo.toml | 5 +++-- src-tauri/src/app.rs | 22 ++++++++++++++++++++++ src-tauri/src/config.rs | 31 +++++++++++++++++++++++++++++++ src-tauri/src/errors.rs | 14 ++++++++++++++ src-tauri/src/ipc.rs | 7 +++++++ src-tauri/src/state.rs | 37 ++++++++++++++++++++++++++++++++++++- src-tauri/src/tray.rs | 16 ++++++++++++---- src-tauri/tauri.conf.json | 7 ++++++- 9 files changed, 149 insertions(+), 17 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a7dbf6d..09f0c33 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "creddy" -version = "0.4.5" +version = "0.4.6" dependencies = [ "argon2", "auto-launch", @@ -1059,6 +1059,7 @@ dependencies = [ "tauri-build", "tauri-plugin-single-instance", "thiserror", + "time", "tokio", "which", "windows 0.51.1", @@ -1202,10 +1203,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ + "powerfmt", "serde", ] @@ -3158,6 +3160,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4536,12 +4544,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa 1.0.9", + "powerfmt", "serde", "time-core", "time-macros", @@ -4549,15 +4558,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a56b2a9..bcfdd75 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "creddy" -version = "0.4.5" +version = "0.4.6" description = "A friendly AWS credentials manager" authors = ["Joseph Montanaro"] license = "" @@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] } +tauri = { version = "1.2", features = [ "app-all", "dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } sodiumoxide = "0.2.7" tokio = { version = ">=1.19", features = ["full"] } @@ -47,6 +47,7 @@ argon2 = { version = "0.5.0", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] } which = "4.4.0" windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] } +time = "0.3.31" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index bc11af5..059f2b9 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -1,4 +1,5 @@ use std::error::Error; +use std::time::Duration; use once_cell::sync::OnceCell; use sqlx::{ @@ -40,6 +41,7 @@ pub fn run() -> tauri::Result<()> { ipc::unlock, ipc::respond, ipc::get_session_status, + ipc::signal_activity, ipc::save_credentials, ipc::get_config, ipc::save_config, @@ -119,10 +121,30 @@ async fn setup(app: &mut App) -> Result<(), Box> { let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome); app.manage(state); + + // make sure we do this after managing app state, so that it doesn't panic + start_auto_locker(app.app_handle()); + Ok(()) } +fn start_auto_locker(app: AppHandle) { + rt::spawn(async move { + let state = app.state::(); + loop { + // this gives our session-timeout a minimum resolution of 10s, which seems fine? + let delay = Duration::from_secs(10); + tokio::time::sleep(delay).await; + + if state.should_auto_lock().await { + state.lock().await.error_popup("Failed to lock Creddy"); + } + } + }); +} + + pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { let w = app.get_window("main").ok_or(WindowError::NoMainWindow)?; w.show()?; diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 5cfb352..ea6dcfc 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::time::Duration; use auto_launch::AutoLaunchBuilder; use is_terminal::IsTerminal; @@ -45,6 +46,10 @@ impl HotkeysConfig { pub struct AppConfig { #[serde(default = "default_rehide_ms")] pub rehide_ms: u64, + #[serde(default = "default_auto_lock")] + pub auto_lock: bool, + #[serde(default = "default_lock_after")] + pub lock_after: Duration, #[serde(default = "default_start_minimized")] pub start_minimized: bool, #[serde(default = "default_start_on_login")] @@ -60,6 +65,8 @@ impl Default for AppConfig { fn default() -> Self { AppConfig { rehide_ms: default_rehide_ms(), + auto_lock: default_auto_lock(), + lock_after: default_lock_after(), start_minimized: default_start_minimized(), start_on_login: default_start_on_login(), terminal: default_term_config(), @@ -187,6 +194,30 @@ fn default_hotkey_config() -> HotkeysConfig { fn default_rehide_ms() -> u64 { 1000 } +fn default_auto_lock() -> bool { true } +fn default_lock_after() -> Duration { Duration::from_secs(43200) } // start minimized and on login only in production mode fn default_start_minimized() -> bool { !cfg!(debug_assertions) } fn default_start_on_login() -> bool { !cfg!(debug_assertions) } + + +// struct DurationVisitor; + +// impl<'de> Visitor<'de> for DurationVisitor { +// type Value = Duration; + +// fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { +// write!(formatter, "an integer between 0 and 2^64 - 1") +// } + +// fn visit_u64(self, v: u64) -> Result { +// Ok(Duration::from_secs(v)) +// } +// } + + +// fn duration_from_secs<'de, D>(deserializer: D) -> Result +// where D: Deserializer<'de> +// { +// deserializer.deserialize_u64(DurationVisitor) +// } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index 96294a8..3621887 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -247,6 +247,19 @@ pub enum UnlockError { } +#[derive(Debug, ThisError, AsRefStr)] +pub enum LockError { + #[error("App is not unlocked")] + NotUnlocked, + #[error("Database error: {0}")] + DbError(#[from] SqlxError), + #[error(transparent)] + Setup(#[from] SetupError), + #[error(transparent)] + TauriError(#[from] tauri::Error), +} + + #[derive(Debug, ThisError, AsRefStr)] pub enum CryptoError { #[error(transparent)] @@ -369,6 +382,7 @@ impl_serialize_basic!(SetupError); impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(ClientInfoError); impl_serialize_basic!(WindowError); +impl_serialize_basic!(LockError); impl Serialize for HandlerError { diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index cf231d3..e6e7ba7 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -56,6 +56,13 @@ pub async fn get_session_status(app_state: State<'_, AppState>) -> Result) -> Result<(), ()> { + app_state.signal_activity().await; + Ok(()) +} + + #[tauri::command] pub async fn save_credentials( credentials: BaseCredentials, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index c410580..5089e5c 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::time::Duration; +use time::OffsetDateTime; use tokio::{ sync::RwLock, @@ -102,6 +103,7 @@ impl VisibilityLease { pub struct AppState { pub config: RwLock, pub session: RwLock, + pub last_activity: RwLock, pub request_count: RwLock, pub waiting_requests: RwLock>>, pub pending_terminal_request: RwLock, @@ -123,6 +125,7 @@ impl AppState { AppState { config: RwLock::new(config), session: RwLock::new(session), + last_activity: RwLock::new(OffsetDateTime::now_utc()), request_count: RwLock::new(0), waiting_requests: RwLock::new(HashMap::new()), pending_terminal_request: RwLock::new(false), @@ -210,6 +213,38 @@ impl AppState { Ok(()) } + pub async fn lock(&self) -> Result<(), LockError> { + let mut session = self.session.write().await; + match *session { + Session::Empty => Err(LockError::NotUnlocked), + Session::Locked(_) => Err(LockError::NotUnlocked), + Session::Unlocked{..} => { + *session = Session::load(&self.pool).await?; + + let app_handle = app::APP.get().unwrap(); + app_handle.emit_all("locked", None::)?; + + Ok(()) + } + } + } + + pub async fn signal_activity(&self) { + let mut last_activity = self.last_activity.write().await; + *last_activity = OffsetDateTime::now_utc(); + } + + pub async fn should_auto_lock(&self) -> bool { + let config = self.config.read().await; + if !config.auto_lock || !self.is_unlocked().await { + return false; + } + + let last_activity = self.last_activity.read().await; + let elapsed = OffsetDateTime::now_utc() - *last_activity; + elapsed >= config.lock_after + } + pub async fn is_unlocked(&self) -> bool { let session = self.session.read().await; matches!(*session, Session::Unlocked{..}) @@ -223,7 +258,7 @@ impl AppState { pub async fn session_creds_cloned(&self) -> Result { let app_session = self.session.read().await; - let (_bsae, session) = app_session.try_get()?; + let (_base, session) = app_session.try_get()?; Ok(session.clone()) } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 2630069..5896a14 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1,12 +1,15 @@ use tauri::{ AppHandle, + CustomMenuItem, + Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, - CustomMenuItem, + async_runtime as rt, }; use crate::app; +use crate::state::AppState; pub fn create() -> SystemTray { @@ -21,13 +24,18 @@ pub fn create() -> SystemTray { } -pub fn handle_event(app: &AppHandle, event: SystemTrayEvent) { +pub fn handle_event(app_handle: &AppHandle, event: SystemTrayEvent) { match event { SystemTrayEvent::MenuItemClick{ id, .. } => { match id.as_str() { - "exit" => app.exit(0), + "exit" => app_handle.exit(0), "show_hide" => { - let _ = app::toggle_main_window(app); + let _ = app::toggle_main_window(app_handle); + let new_handle = app_handle.app_handle(); + rt::spawn(async move { + let state = new_handle.state::(); + state.signal_activity().await; + }); } _ => (), } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3b080db..0b64028 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,10 +8,15 @@ }, "package": { "productName": "creddy", - "version": "0.4.5" + "version": "0.4.6" }, "tauri": { "allowlist": { + "app": { + "all": true, + "show": false, + "hide": false + }, "os": {"all": true}, "dialog": {"open": true} },