From 647c08c5b8f4d52c8b35e2af428bb7afb82190ab Mon Sep 17 00:00:00 2001 From: Vincent Rozenberg Date: Thu, 3 Sep 2026 13:35:26 +0200 Subject: [PATCH] Fix fill-password: wrong host key, and silent failure Two bugs made 'Fill saved password' do nothing: 1. HOST MISMATCH. Saving used location.hostname at submit time (e.g. accounts.google.com); filling derived the host from the app's configured URL (mail.google.com). The Keychain lookup always found nothing for any service with a separate auth domain. Fixed by storing the save-time hostname in App.saved_host and reading it back at fill time, falling back to default_scope for entries saved before this. 2. SILENT FAILURE. Every error path in the fillpw sentinel called only eprintln!, which is invisible to the user. A feature that fails silently is indistinguishable from one that is not wired up. Fixed by emitting a fill-error event; the shell shows it as a toast in the nav column, which is left of the stage and therefore always above native views. --- src-tauri/src/commands.rs | 14 +++++++++++++- src-tauri/src/config.rs | 7 +++++++ src-tauri/src/webviews.rs | 3 ++- src/App.tsx | 21 +++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 54f3833..f560542 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -333,6 +333,7 @@ pub fn add_app( hidden: Vec::new(), icon: None, saved_account: None, + saved_host: None, zoom: 1.0, order, }; @@ -947,6 +948,7 @@ pub fn save_password(state: State<'_, AppState>) -> Result { let mut cfg = state.config.lock().unwrap(); if let Some(a) = cfg.apps.iter_mut().find(|a| a.id == app_id) { a.saved_account = Some(account); + a.saved_host = Some(host.clone()); } } state.persist()?; @@ -977,7 +979,17 @@ pub fn fill_password_for( .saved_account .clone() .ok_or("no password saved for this app")?; - let host = config::default_scope(&target.url).ok_or("this app has no host")?; + // Prefer the hostname that was live when the credential was saved + // (`location.hostname` at submit time). For services with a separate auth + // domain (Google, Microsoft, Okta…) that differs from the app's configured + // URL, so looking up by the configured URL's host always finds nothing. + // Fall back to deriving from the URL for entries saved before this field + // was added. + let host = target + .saved_host + .clone() + .or_else(|| config::default_scope(&target.url)) + .ok_or("this app has no host")?; #[cfg(target_os = "macos")] let password = { diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index f34b28c..085183c 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -41,6 +41,12 @@ pub struct App { /// the Keychain — but without it the Keychain cannot be asked for the item. #[serde(default)] pub saved_account: Option, + /// The hostname the credential was saved under — `location.hostname` at + /// submit time, which is often different from the app's configured URL + /// (e.g. `accounts.google.com` vs `mail.google.com`). Stored so the fill + /// path can look up the same key the save path wrote. + #[serde(default)] + pub saved_host: Option, /// Page zoom, remembered per app: a dense ERP and a mail client do not /// want the same size. #[serde(default = "default_zoom")] @@ -221,6 +227,7 @@ pub fn seed() -> Config { hidden: Vec::new(), icon: None, saved_account: None, + saved_host: None, zoom: 1.0, order, }; diff --git a/src-tauri/src/webviews.rs b/src-tauri/src/webviews.rs index a3cd8cf..063f1a4 100644 --- a/src-tauri/src/webviews.rs +++ b/src-tauri/src/webviews.rs @@ -295,7 +295,7 @@ fn handle_sentinel( "fillpw" => { let state = handle.state::(); if let Err(e) = crate::commands::fill_password_for(&handle, &from, &state) { - eprintln!("could not fill a password: {e}"); + let _ = handle.emit("fill-error", e); } } @@ -1084,6 +1084,7 @@ mod tests { hidden: vec![".ad".into()], icon: None, saved_account: None, + saved_host: None, zoom: 1.0, order: 0, }; diff --git a/src/App.tsx b/src/App.tsx index 23150ce..5ce0291 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ export default function App() { const [downloads, setDownloads] = useState([]); const [offer, setOffer] = useState(null); const [saved, setSaved] = useState(null); + const [fillError, setFillError] = useState(null); const stageRef = useRef(null); const booted = useRef(false); @@ -147,6 +148,11 @@ export default function App() { }), // A login was submitted. The password is held in Rust; this only asks. listen("password-offer", (e) => setOffer(e.payload)), + // Fill failed — show the reason rather than doing nothing silently. + listen("fill-error", (e) => { + setFillError(e.payload); + setTimeout(() => setFillError(null), 5000); + }), listen<{ id: number; name: string; path: string }>("download-started", (e) => setDownloads((d) => [ @@ -322,6 +328,21 @@ export default function App() { )} + {fillError && ( + /* Positioned within the nav column (left of the stage), so it is + always above native views. The stage starts at x = collapsed ? rail + : PANEL; this toast stays well within that. */ +
+
+ {fillError} +
+
+ )} + {settingsOpen && (