diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 23304ebbc..3d07e9514 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -6,8 +6,20 @@ fn merge_settings_for_save( mut incoming: crate::settings::AppSettings, existing: &crate::settings::AppSettings, ) -> crate::settings::AppSettings { - if incoming.webdav_sync.is_none() { - incoming.webdav_sync = existing.webdav_sync.clone(); + match (&mut incoming.webdav_sync, &existing.webdav_sync) { + // incoming 没有 webdav → 保留现有 + (None, _) => { + incoming.webdav_sync = existing.webdav_sync.clone(); + } + // incoming 有 webdav 但密码为空,且现有有密码 → 填回现有密码 + // (get_settings_for_frontend 总是清空密码,所以通过 save_settings + // 传入的空密码意味着"保持现有"而非"用户主动清空") + (Some(incoming_sync), Some(existing_sync)) + if incoming_sync.password.is_empty() && !existing_sync.password.is_empty() => + { + incoming_sync.password = existing_sync.password.clone(); + } + _ => {} } incoming } @@ -116,6 +128,66 @@ mod tests { Some("https://dav.new.example.com") ); } + + /// Regression test: frontend always receives empty password from + /// get_settings_for_frontend(). If a component accidentally spreads + /// the full settings object into save_settings, the empty password + /// must NOT overwrite the existing one. + #[test] + fn save_settings_should_preserve_password_when_incoming_has_empty_password() { + let mut existing = AppSettings::default(); + existing.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: "secret".to_string(), + ..WebDavSyncSettings::default() + }); + + // Simulate frontend sending settings with cleared password + let mut incoming = AppSettings::default(); + incoming.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: "".to_string(), + ..WebDavSyncSettings::default() + }); + + let merged = merge_settings_for_save(incoming, &existing); + + assert_eq!( + merged.webdav_sync.as_ref().map(|v| v.password.as_str()), + Some("secret"), + "empty password from frontend must not overwrite existing password" + ); + } + + /// When both incoming and existing have no password, merge should + /// work without panicking and keep the empty state. + #[test] + fn save_settings_should_handle_both_empty_passwords() { + let mut existing = AppSettings::default(); + existing.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: "".to_string(), + ..WebDavSyncSettings::default() + }); + + let mut incoming = AppSettings::default(); + incoming.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: "".to_string(), + ..WebDavSyncSettings::default() + }); + + let merged = merge_settings_for_save(incoming, &existing); + + assert_eq!( + merged.webdav_sync.as_ref().map(|v| v.password.as_str()), + Some("") + ); + } } /// 获取开机自启状态 diff --git a/src/components/UsageScriptModal.tsx b/src/components/UsageScriptModal.tsx index c248b52c7..87692cb37 100644 --- a/src/components/UsageScriptModal.tsx +++ b/src/components/UsageScriptModal.tsx @@ -267,7 +267,8 @@ const UsageScriptModal: React.FC = ({ setShowUsageConfirm(false); try { if (settingsData) { - await settingsApi.save({ ...settingsData, usageConfirmed: true }); + const { webdavSync: _, ...rest } = settingsData; + await settingsApi.save({ ...rest, usageConfirmed: true }); await queryClient.invalidateQueries({ queryKey: ["settings"] }); } } catch (error) { diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 6a283a0b2..73818b941 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -204,7 +204,8 @@ export function ProviderList({ setShowStreamCheckConfirm(false); try { if (settings) { - await settingsApi.save({ ...settings, streamCheckConfirmed: true }); + const { webdavSync: _, ...rest } = settings; + await settingsApi.save({ ...rest, streamCheckConfirmed: true }); await queryClient.invalidateQueries({ queryKey: ["settings"] }); } } catch (error) {