fix(qbzd): file-based token fallback when keyring unavailable

On headless systems without gnome-keyring/kwallet, the keyring
fails with ServiceUnknown. Now saves token to ~/.local/share/qbz/.oauth-token
(mode 0600) as fallback. Load also checks file after keyring.

This fixes the 'login works but credentials lost on restart' bug
on headless machines.

cc
This commit is contained in:
vicrodh committed 2026-04-09 10:57:30 -06:00
1 parent fd1a4fade1
commit 7a9f8407a8
2 files changed
+65 -23

No files matched your search

+23 -16
View File
@@ -217,28 +217,35 @@ async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> Option<u64> {
}
}
/// Load OAuth token from system keyring.
/// Uses the same service/key as the desktop app so credentials are shared.
/// Load OAuth token — tries keyring first, then file fallback.
fn load_oauth_token() -> Option<String> {
const SERVICE: &str = "qbz-player";
const KEY: &str = "qobuz-oauth-token";
let entry = keyring::Entry::new(SERVICE, KEY).ok()?;
match entry.get_password() {
Ok(token) if !token.is_empty() => {
log::info!("[qbzd] OAuth token loaded from keyring");
Some(token)
}
Ok(_) => None,
Err(keyring::Error::NoEntry) => {
log::debug!("[qbzd] No OAuth token in keyring");
None
}
Err(e) => {
log::warn!("[qbzd] Keyring access failed: {}", e);
None
// Try keyring first
if let Ok(entry) = keyring::Entry::new(SERVICE, KEY) {
match entry.get_password() {
Ok(token) if !token.is_empty() => {
log::info!("[qbzd] OAuth token loaded from keyring");
return Some(token);
}
Ok(_) => {}
Err(keyring::Error::NoEntry) => {
log::debug!("[qbzd] No OAuth token in keyring");
}
Err(e) => {
log::warn!("[qbzd] Keyring access failed: {}", e);
}
}
}
// Fallback: try file
if let Some(token) = crate::login::load_token_from_file() {
log::info!("[qbzd] OAuth token loaded from file fallback");
return Some(token);
}
None
}
/// Register the daemon as a `_qbz._tcp` mDNS service for LAN discovery.
+42 -7
View File
@@ -114,21 +114,56 @@ pub async fn interactive_login() -> Result<(), String> {
Ok(())
}
/// Save OAuth token to system keyring (same service/key as desktop app).
/// Save OAuth token — tries keyring first, falls back to encrypted file.
fn save_token_to_keyring(token: &str) -> Result<(), String> {
const SERVICE: &str = "qbz-player";
const KEY: &str = "qobuz-oauth-token";
let entry = keyring::Entry::new(SERVICE, KEY)
.map_err(|e| format!("Keyring error: {}", e))?;
entry
.set_password(token)
.map_err(|e| format!("Failed to save to keyring: {}", e))?;
// Try keyring first
match keyring::Entry::new(SERVICE, KEY) {
Ok(entry) => match entry.set_password(token) {
Ok(()) => {
println!("Token saved to system keyring");
return Ok(());
}
Err(e) => {
println!("Keyring unavailable ({}), using file fallback", e);
}
},
Err(e) => {
println!("Keyring unavailable ({}), using file fallback", e);
}
}
println!("Token saved to system keyring (service: {}, key: {})", SERVICE, KEY);
// Fallback: save to file
save_token_to_file(token)
}
fn token_file_path() -> Option<std::path::PathBuf> {
dirs::data_dir().map(|d| d.join("qbz").join(".oauth-token"))
}
fn save_token_to_file(token: &str) -> Result<(), String> {
let path = token_file_path().ok_or("Cannot determine data directory")?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(&path, token).map_err(|e| format!("Failed to write token file: {}", e))?;
// Restrict permissions on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
}
println!("Token saved to {}", path.display());
Ok(())
}
pub fn load_token_from_file() -> Option<String> {
let path = token_file_path()?;
std::fs::read_to_string(&path).ok().filter(|t| !t.trim().is_empty())
}
/// Detect the primary LAN IP address of this machine.
fn detect_lan_ip() -> Option<String> {
// Try to get the default route interface IP by connecting to a public DNS