Remove unused API Keys UI and infrastructure

All API requests now go through Cloudflare Workers proxy.
User-provided credentials were collected but never used.

Removed:
- src-tauri/src/api_keys.rs (entire module)
- API Keys section from Settings UI
- ApiKeysState and related commands
- User credential parameters from providers
- Simplified DiscogsClient (removed with_user_credentials)
- Cleaned up imports and translations

Moved documentation:
- CHANGELOG-api-keys-refactor.md
- HANDOFF-MERGE.md
- MERGE-PREP.md
- POST-MERGE-CLEANUP.md
Moved to: qbz-nix-docs/feature-api-keys-refactor/

This cleanup reduces codebase by ~715 lines and removes
confusing UI that promised functionality that didn't work.
This commit is contained in:
vicrodh committed 2026-01-17 17:09:49 -06:00
1 parent 7631462522
commit cac5a0c173
15 files changed
+24 -2420

No files matched your search

-174
View File
@@ -1,174 +0,0 @@
//! User-provided API credentials store
//!
//! Manages runtime credentials that override embedded (build-time) credentials.
//! Users can provide their own API keys if the embedded ones stop working.
use std::sync::Arc;
use tokio::sync::Mutex;
/// Credentials for a specific provider
#[derive(Debug, Clone, Default)]
pub struct ProviderCredentials {
pub client_id: Option<String>,
pub client_secret: Option<String>,
}
impl ProviderCredentials {
pub fn is_set(&self) -> bool {
self.client_id.is_some() && self.client_secret.is_some()
}
}
/// Store for all user-provided API credentials
#[derive(Debug, Default)]
pub struct ApiKeysStore {
pub spotify: ProviderCredentials,
pub tidal: ProviderCredentials,
pub discogs: ProviderCredentials,
}
impl ApiKeysStore {
pub fn new() -> Self {
Self::default()
}
}
/// Thread-safe wrapper for the API keys store
pub type ApiKeysState = Arc<Mutex<ApiKeysStore>>;
pub fn create_api_keys_state() -> ApiKeysState {
Arc::new(Mutex::new(ApiKeysStore::new()))
}
// Tauri commands
/// Set Spotify credentials
#[tauri::command]
pub async fn set_spotify_credentials(
client_id: String,
client_secret: String,
state: tauri::State<'_, ApiKeysState>,
) -> Result<(), String> {
log::info!("Setting user-provided Spotify credentials");
let mut store = state.lock().await;
store.spotify = ProviderCredentials {
client_id: Some(client_id),
client_secret: Some(client_secret),
};
Ok(())
}
/// Clear Spotify credentials
#[tauri::command]
pub async fn clear_spotify_credentials(
state: tauri::State<'_, ApiKeysState>,
) -> Result<(), String> {
log::info!("Clearing user-provided Spotify credentials");
let mut store = state.lock().await;
store.spotify = ProviderCredentials::default();
Ok(())
}
/// Check if Spotify has user-provided credentials
#[tauri::command]
pub async fn has_spotify_user_credentials(
state: tauri::State<'_, ApiKeysState>,
) -> Result<bool, String> {
let store = state.lock().await;
Ok(store.spotify.is_set())
}
/// Set Tidal credentials
#[tauri::command]
pub async fn set_tidal_credentials(
client_id: String,
client_secret: String,
state: tauri::State<'_, ApiKeysState>,
) -> Result<(), String> {
log::info!("Setting user-provided Tidal credentials");
let mut store = state.lock().await;
store.tidal = ProviderCredentials {
client_id: Some(client_id),
client_secret: Some(client_secret),
};
Ok(())
}
/// Clear Tidal credentials
#[tauri::command]
pub async fn clear_tidal_credentials(
state: tauri::State<'_, ApiKeysState>,
) -> Result<(), String> {
log::info!("Clearing user-provided Tidal credentials");
let mut store = state.lock().await;
store.tidal = ProviderCredentials::default();
Ok(())
}
/// Check if Tidal has user-provided credentials
#[tauri::command]
pub async fn has_tidal_user_credentials(
state: tauri::State<'_, ApiKeysState>,
) -> Result<bool, String> {
let store = state.lock().await;
Ok(store.tidal.is_set())
}
/// Set Discogs credentials
#[tauri::command]
pub async fn set_discogs_credentials(
consumer_key: String,
consumer_secret: String,
state: tauri::State<'_, ApiKeysState>,
) -> Result<(), String> {
log::info!("Setting user-provided Discogs credentials");
let mut store = state.lock().await;
store.discogs = ProviderCredentials {
client_id: Some(consumer_key),
client_secret: Some(consumer_secret),
};
Ok(())
}
/// Clear Discogs credentials
#[tauri::command]
pub async fn clear_discogs_credentials(
state: tauri::State<'_, ApiKeysState>,
) -> Result<(), String> {
log::info!("Clearing user-provided Discogs credentials");
let mut store = state.lock().await;
store.discogs = ProviderCredentials::default();
Ok(())
}
/// Check if Discogs has user-provided credentials
#[tauri::command]
pub async fn has_discogs_user_credentials(
state: tauri::State<'_, ApiKeysState>,
) -> Result<bool, String> {
let store = state.lock().await;
Ok(store.discogs.is_set())
}
/// Check which providers have embedded credentials available
#[tauri::command]
pub fn get_embedded_credentials_status() -> EmbeddedCredentialsStatus {
EmbeddedCredentialsStatus {
spotify: option_env!("SPOTIFY_API_CLIENT_ID").is_some()
&& option_env!("SPOTIFY_API_CLIENT_SECRET").is_some(),
tidal: option_env!("TIDAL_API_CLIENT_ID").is_some()
&& option_env!("TIDAL_API_CLIENT_SECRET").is_some(),
discogs: option_env!("DISCOGS_API_CLIENT_KEY").is_some()
&& option_env!("DISCOGS_API_CLIENT_SECRET").is_some(),
lastfm: option_env!("LAST_FM_API_KEY").is_some()
&& option_env!("LAST_FM_API_SHARED_SECRET").is_some(),
}
}
#[derive(serde::Serialize)]
pub struct EmbeddedCredentialsStatus {
pub spotify: bool,
pub tidal: bool,
pub discogs: bool,
pub lastfm: bool,
}
+3 -47
View File
@@ -2,41 +2,18 @@
use tauri::State;
use crate::api_keys::ApiKeysState;
use crate::playlist_import::{
import_public_playlist, preview_public_playlist, ImportPlaylist, ImportSummary,
ProviderCredentials,
};
use crate::AppState;
#[tauri::command]
pub async fn playlist_import_preview(
url: String,
api_keys: State<'_, ApiKeysState>,
) -> Result<ImportPlaylist, String> {
log::info!("Command: playlist_import_preview {}", url);
// Get user-provided credentials from state
let keys = api_keys.lock().await;
let spotify_creds = if keys.spotify.is_set() {
Some(ProviderCredentials {
client_id: keys.spotify.client_id.clone(),
client_secret: keys.spotify.client_secret.clone(),
})
} else {
None
};
let tidal_creds = if keys.tidal.is_set() {
Some(ProviderCredentials {
client_id: keys.tidal.client_id.clone(),
client_secret: keys.tidal.client_secret.clone(),
})
} else {
None
};
drop(keys);
preview_public_playlist(&url, spotify_creds, tidal_creds)
preview_public_playlist(&url, None, None)
.await
.map_err(|e| e.to_string())
}
@@ -47,38 +24,17 @@ pub async fn playlist_import_execute(
name_override: Option<String>,
is_public: Option<bool>,
state: State<'_, AppState>,
api_keys: State<'_, ApiKeysState>,
) -> Result<ImportSummary, String> {
log::info!("Command: playlist_import_execute {}", url);
// Get user-provided credentials from state
let keys = api_keys.lock().await;
let spotify_creds = if keys.spotify.is_set() {
Some(ProviderCredentials {
client_id: keys.spotify.client_id.clone(),
client_secret: keys.spotify.client_secret.clone(),
})
} else {
None
};
let tidal_creds = if keys.tidal.is_set() {
Some(ProviderCredentials {
client_id: keys.tidal.client_id.clone(),
client_secret: keys.tidal.client_secret.clone(),
})
} else {
None
};
drop(keys);
let client = state.client.lock().await;
import_public_playlist(
&url,
&client,
name_override.as_deref(),
is_public.unwrap_or(false),
spotify_creds,
tidal_creds,
None,
None,
)
.await
.map_err(|e| e.to_string())
-8
View File
@@ -66,14 +66,6 @@ struct ReleaseImage {
impl DiscogsClient {
/// Create a new Discogs client (proxy handles credentials)
pub fn new() -> Self {
Self::with_user_credentials(None, None)
}
/// Create a new Discogs client - compatibility method (proxy handles credentials)
pub fn with_user_credentials(
_user_key: Option<String>,
_user_secret: Option<String>,
) -> Self {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::USER_AGENT,
-15
View File
@@ -5,7 +5,6 @@
pub mod api;
pub mod api_cache;
pub mod api_keys;
pub mod audio;
pub mod cache;
pub mod cast;
@@ -166,8 +165,6 @@ pub fn run() {
// Initialize download settings state
let download_settings_state = config::download_settings::create_download_settings_state()
.expect("Failed to initialize download settings");
// Initialize API keys state (for user-provided credentials)
let api_keys_state = api_keys::create_api_keys_state();
// Initialize offline mode state
let offline_state = offline::OfflineState::new()
.expect("Failed to initialize offline state");
@@ -294,7 +291,6 @@ pub fn run() {
.manage(session_store_state)
.manage(audio_settings_state)
.manage(download_settings_state)
.manage(api_keys_state)
.manage(offline_state)
.invoke_handler(tauri::generate_handler![
// Auth commands
@@ -558,17 +554,6 @@ pub fn run() {
config::download_settings::set_download_root,
config::download_settings::set_show_downloads_in_library,
config::download_settings::validate_download_root,
// API keys commands (user-provided credentials)
api_keys::set_spotify_credentials,
api_keys::clear_spotify_credentials,
api_keys::has_spotify_user_credentials,
api_keys::set_tidal_credentials,
api_keys::clear_tidal_credentials,
api_keys::has_tidal_user_credentials,
api_keys::set_discogs_credentials,
api_keys::clear_discogs_credentials,
api_keys::has_discogs_user_credentials,
api_keys::get_embedded_credentials_status,
// Offline mode commands
offline::commands::get_offline_status,
offline::commands::get_offline_settings,
+10 -51
View File
@@ -7,7 +7,6 @@ use std::sync::Arc;
use tauri::State;
use tokio::sync::Mutex;
use crate::api_keys::ApiKeysState;
use crate::discogs::DiscogsClient;
use crate::library::{
cue_to_tracks, get_artwork_cache_dir, CueParser, LibraryDatabase, LibraryFolder, LibraryScanner, LibraryStats,
@@ -1188,21 +1187,11 @@ pub async fn playlist_increment_play_count(
// === Discogs Artwork ===
/// Check if Discogs credentials are configured (embedded or user-provided)
/// Check if Discogs credentials are configured (proxy handles credentials)
#[tauri::command]
pub async fn discogs_has_credentials(
api_keys: State<'_, ApiKeysState>,
) -> Result<bool, String> {
// Check user-provided credentials first
let keys = api_keys.lock().await;
if keys.discogs.is_set() {
return Ok(true);
}
drop(keys);
// Fall back to embedded/env credentials
let client = DiscogsClient::new();
Ok(client.has_credentials())
pub async fn discogs_has_credentials() -> Result<bool, String> {
// Proxy always provides credentials
Ok(true)
}
/// Fetch missing artwork from Discogs for albums without artwork
@@ -1210,21 +1199,11 @@ pub async fn discogs_has_credentials(
#[tauri::command]
pub async fn library_fetch_missing_artwork(
state: State<'_, LibraryState>,
api_keys: State<'_, ApiKeysState>,
) -> Result<u32, String> {
log::info!("Command: library_fetch_missing_artwork");
// Get user-provided credentials if available
let keys = api_keys.lock().await;
let discogs = DiscogsClient::with_user_credentials(
keys.discogs.client_id.clone(),
keys.discogs.client_secret.clone(),
);
drop(keys);
if !discogs.has_credentials() {
return Err("Discogs credentials not configured".to_string());
}
// Get Discogs client (proxy handles credentials)
let discogs = DiscogsClient::new();
let artwork_cache = get_artwork_cache_dir();
let mut updated_count = 0u32;
@@ -1263,21 +1242,11 @@ pub async fn library_fetch_album_artwork(
artist: String,
album: String,
state: State<'_, LibraryState>,
api_keys: State<'_, ApiKeysState>,
) -> Result<Option<String>, String> {
log::info!("Command: library_fetch_album_artwork {} - {}", artist, album);
// Get user-provided credentials if available
let keys = api_keys.lock().await;
let discogs = DiscogsClient::with_user_credentials(
keys.discogs.client_id.clone(),
keys.discogs.client_secret.clone(),
);
drop(keys);
if !discogs.has_credentials() {
return Err("Discogs credentials not configured".to_string());
}
// Get Discogs client (proxy handles credentials)
let discogs = DiscogsClient::new();
let artwork_cache = get_artwork_cache_dir();
@@ -1335,21 +1304,11 @@ pub async fn library_set_album_artwork(
#[tauri::command]
pub async fn discogs_search_artist(
query: String,
api_keys: State<'_, ApiKeysState>,
) -> Result<crate::discogs::SearchResponse, String> {
log::info!("Command: discogs_search_artist query={}", query);
// Get user-provided credentials if available
let keys = api_keys.lock().await;
let discogs = DiscogsClient::with_user_credentials(
keys.discogs.client_id.clone(),
keys.discogs.client_secret.clone(),
);
drop(keys);
if !discogs.has_credentials() {
return Err("Discogs credentials not configured".to_string());
}
// Get Discogs client (proxy handles credentials)
let discogs = DiscogsClient::new();
discogs.search_artist(&query).await
}
@@ -40,21 +40,19 @@ pub fn detect_provider(url: &str) -> Result<ProviderKind, PlaylistImportError> {
Err(PlaylistImportError::UnsupportedProvider(url.to_string()))
}
/// Fetch playlist with optional user-provided credentials
/// Fetch playlist (proxy handles credentials)
pub async fn fetch_playlist(
kind: ProviderKind,
spotify_creds: Option<ProviderCredentials>,
tidal_creds: Option<ProviderCredentials>,
) -> Result<ImportPlaylist, PlaylistImportError> {
match kind {
ProviderKind::Spotify { playlist_id } => {
spotify::fetch_playlist(&playlist_id, spotify_creds).await
spotify::fetch_playlist(&playlist_id).await
}
ProviderKind::AppleMusic { storefront, playlist_id } => {
apple::fetch_playlist(&storefront, &playlist_id).await
}
ProviderKind::Tidal { playlist_id } => {
tidal::fetch_playlist(&playlist_id, tidal_creds).await
tidal::fetch_playlist(&playlist_id).await
}
ProviderKind::Deezer { playlist_id } => deezer::fetch_playlist(&playlist_id).await,
}
@@ -38,9 +38,8 @@ pub fn parse_playlist_id(url: &str) -> Option<String> {
pub async fn fetch_playlist(
playlist_id: &str,
user_creds: Option<ProviderCredentials>,
) -> Result<ImportPlaylist, PlaylistImportError> {
if let Ok(token) = get_app_token(user_creds).await {
if let Ok(token) = get_app_token().await {
if let Ok(playlist) = fetch_playlist_with_token(playlist_id, &token).await {
return Ok(playlist);
}
@@ -249,8 +248,8 @@ async fn fetch_playlist_from_embed(playlist_id: &str) -> Result<ImportPlaylist,
})
}
async fn get_app_token(_user_creds: Option<ProviderCredentials>) -> Result<String, PlaylistImportError> {
// Proxy handles credentials - user_creds ignored (compatibility)
async fn get_app_token() -> Result<String, PlaylistImportError> {
// Proxy handles credentials
let url = format!("{}/token", SPOTIFY_PROXY_URL);
let response: Value = reqwest::Client::builder()
@@ -39,9 +39,8 @@ pub fn parse_playlist_id(url: &str) -> Option<String> {
pub async fn fetch_playlist(
playlist_id: &str,
user_creds: Option<ProviderCredentials>,
) -> Result<ImportPlaylist, PlaylistImportError> {
let token = get_app_token(user_creds).await?;
let token = get_app_token().await?;
let country_code = env::var("TIDAL_COUNTRY_CODE").unwrap_or_else(|_| "US".to_string());
let client = reqwest::Client::new();
@@ -331,8 +330,8 @@ fn parse_duration_ms(value: &str) -> Option<u64> {
}
}
async fn get_app_token(_user_creds: Option<ProviderCredentials>) -> Result<String, PlaylistImportError> {
// Proxy handles credentials - user_creds ignored (compatibility)
async fn get_app_token() -> Result<String, PlaylistImportError> {
// Proxy handles credentials
let url = format!("{}/token", TIDAL_PROXY_URL);
let response: Value = reqwest::Client::builder()