add idle timeout and version on settings screen

This commit is contained in:
Joseph Montanaro 2024-01-31 13:14:08 -08:00
parent 69f6a39396
commit 141334f7e2
9 changed files with 149 additions and 17 deletions

27
src-tauri/Cargo.lock generated
View File

@ -1035,7 +1035,7 @@ dependencies = [
[[package]] [[package]]
name = "creddy" name = "creddy"
version = "0.4.5" version = "0.4.6"
dependencies = [ dependencies = [
"argon2", "argon2",
"auto-launch", "auto-launch",
@ -1059,6 +1059,7 @@ dependencies = [
"tauri-build", "tauri-build",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"which", "which",
"windows 0.51.1", "windows 0.51.1",
@ -1202,10 +1203,11 @@ dependencies = [
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.8" version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [ dependencies = [
"powerfmt",
"serde", "serde",
] ]
@ -3158,6 +3160,12 @@ dependencies = [
"universal-hash", "universal-hash",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -4536,12 +4544,13 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.28" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa 1.0.9", "itoa 1.0.9",
"powerfmt",
"serde", "serde",
"time-core", "time-core",
"time-macros", "time-macros",
@ -4549,15 +4558,15 @@ dependencies = [
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.14" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "creddy" name = "creddy"
version = "0.4.5" version = "0.4.6"
description = "A friendly AWS credentials manager" description = "A friendly AWS credentials manager"
authors = ["Joseph Montanaro"] authors = ["Joseph Montanaro"]
license = "" license = ""
@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } 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" } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
sodiumoxide = "0.2.7" sodiumoxide = "0.2.7"
tokio = { version = ">=1.19", features = ["full"] } tokio = { version = ">=1.19", features = ["full"] }
@ -47,6 +47,7 @@ argon2 = { version = "0.5.0", features = ["std"] }
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] }
which = "4.4.0" which = "4.4.0"
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] } windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
time = "0.3.31"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode

View File

@ -1,4 +1,5 @@
use std::error::Error; use std::error::Error;
use std::time::Duration;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use sqlx::{ use sqlx::{
@ -40,6 +41,7 @@ pub fn run() -> tauri::Result<()> {
ipc::unlock, ipc::unlock,
ipc::respond, ipc::respond,
ipc::get_session_status, ipc::get_session_status,
ipc::signal_activity,
ipc::save_credentials, ipc::save_credentials,
ipc::get_config, ipc::get_config,
ipc::save_config, ipc::save_config,
@ -119,10 +121,30 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome); let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome);
app.manage(state); 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(()) Ok(())
} }
fn start_auto_locker(app: AppHandle) {
rt::spawn(async move {
let state = app.state::<AppState>();
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> { pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
let w = app.get_window("main").ok_or(WindowError::NoMainWindow)?; let w = app.get_window("main").ok_or(WindowError::NoMainWindow)?;
w.show()?; w.show()?;

View File

@ -1,4 +1,5 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
use auto_launch::AutoLaunchBuilder; use auto_launch::AutoLaunchBuilder;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
@ -45,6 +46,10 @@ impl HotkeysConfig {
pub struct AppConfig { pub struct AppConfig {
#[serde(default = "default_rehide_ms")] #[serde(default = "default_rehide_ms")]
pub rehide_ms: u64, 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")] #[serde(default = "default_start_minimized")]
pub start_minimized: bool, pub start_minimized: bool,
#[serde(default = "default_start_on_login")] #[serde(default = "default_start_on_login")]
@ -60,6 +65,8 @@ impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
AppConfig { AppConfig {
rehide_ms: default_rehide_ms(), rehide_ms: default_rehide_ms(),
auto_lock: default_auto_lock(),
lock_after: default_lock_after(),
start_minimized: default_start_minimized(), start_minimized: default_start_minimized(),
start_on_login: default_start_on_login(), start_on_login: default_start_on_login(),
terminal: default_term_config(), terminal: default_term_config(),
@ -187,6 +194,30 @@ fn default_hotkey_config() -> HotkeysConfig {
fn default_rehide_ms() -> u64 { 1000 } 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 // start minimized and on login only in production mode
fn default_start_minimized() -> bool { !cfg!(debug_assertions) } fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
fn default_start_on_login() -> 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<E: de::Error>(self, v: u64) -> Result<Duration, E> {
// Ok(Duration::from_secs(v))
// }
// }
// fn duration_from_secs<'de, D>(deserializer: D) -> Result<Duration, D::Error>
// where D: Deserializer<'de>
// {
// deserializer.deserialize_u64(DurationVisitor)
// }

View File

@ -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)] #[derive(Debug, ThisError, AsRefStr)]
pub enum CryptoError { pub enum CryptoError {
#[error(transparent)] #[error(transparent)]
@ -369,6 +382,7 @@ impl_serialize_basic!(SetupError);
impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(GetCredentialsError);
impl_serialize_basic!(ClientInfoError); impl_serialize_basic!(ClientInfoError);
impl_serialize_basic!(WindowError); impl_serialize_basic!(WindowError);
impl_serialize_basic!(LockError);
impl Serialize for HandlerError { impl Serialize for HandlerError {

View File

@ -56,6 +56,13 @@ pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String
} }
#[tauri::command]
pub async fn signal_activity(app_state: State<'_, AppState>) -> Result<(), ()> {
app_state.signal_activity().await;
Ok(())
}
#[tauri::command] #[tauri::command]
pub async fn save_credentials( pub async fn save_credentials(
credentials: BaseCredentials, credentials: BaseCredentials,

View File

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use time::OffsetDateTime;
use tokio::{ use tokio::{
sync::RwLock, sync::RwLock,
@ -102,6 +103,7 @@ impl VisibilityLease {
pub struct AppState { pub struct AppState {
pub config: RwLock<AppConfig>, pub config: RwLock<AppConfig>,
pub session: RwLock<Session>, pub session: RwLock<Session>,
pub last_activity: RwLock<OffsetDateTime>,
pub request_count: RwLock<u64>, pub request_count: RwLock<u64>,
pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>, pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>,
pub pending_terminal_request: RwLock<bool>, pub pending_terminal_request: RwLock<bool>,
@ -123,6 +125,7 @@ impl AppState {
AppState { AppState {
config: RwLock::new(config), config: RwLock::new(config),
session: RwLock::new(session), session: RwLock::new(session),
last_activity: RwLock::new(OffsetDateTime::now_utc()),
request_count: RwLock::new(0), request_count: RwLock::new(0),
waiting_requests: RwLock::new(HashMap::new()), waiting_requests: RwLock::new(HashMap::new()),
pending_terminal_request: RwLock::new(false), pending_terminal_request: RwLock::new(false),
@ -210,6 +213,38 @@ impl AppState {
Ok(()) 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::<usize>)?;
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 { pub async fn is_unlocked(&self) -> bool {
let session = self.session.read().await; let session = self.session.read().await;
matches!(*session, Session::Unlocked{..}) matches!(*session, Session::Unlocked{..})
@ -223,7 +258,7 @@ impl AppState {
pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> { pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
let app_session = self.session.read().await; let app_session = self.session.read().await;
let (_bsae, session) = app_session.try_get()?; let (_base, session) = app_session.try_get()?;
Ok(session.clone()) Ok(session.clone())
} }

View File

@ -1,12 +1,15 @@
use tauri::{ use tauri::{
AppHandle, AppHandle,
CustomMenuItem,
Manager,
SystemTray, SystemTray,
SystemTrayEvent, SystemTrayEvent,
SystemTrayMenu, SystemTrayMenu,
CustomMenuItem, async_runtime as rt,
}; };
use crate::app; use crate::app;
use crate::state::AppState;
pub fn create() -> SystemTray { 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 { match event {
SystemTrayEvent::MenuItemClick{ id, .. } => { SystemTrayEvent::MenuItemClick{ id, .. } => {
match id.as_str() { match id.as_str() {
"exit" => app.exit(0), "exit" => app_handle.exit(0),
"show_hide" => { "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::<AppState>();
state.signal_activity().await;
});
} }
_ => (), _ => (),
} }

View File

@ -8,10 +8,15 @@
}, },
"package": { "package": {
"productName": "creddy", "productName": "creddy",
"version": "0.4.5" "version": "0.4.6"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
"app": {
"all": true,
"show": false,
"hide": false
},
"os": {"all": true}, "os": {"all": true},
"dialog": {"open": true} "dialog": {"open": true}
}, },