From ac5a0a336795f3e5270f5958d16d7461b61837db Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 01:11:42 +0200 Subject: [PATCH] feat: Settings persistent + Auto-Login + Installer Update-Modus Settings-Persistenz: - Config wird in OS-AppData gespeichert (Windows: %APPDATA%/MiniCloud Sync/config.json, Linux: ~/.config/MiniCloud Sync/config.json, Mac: ~/Library/Application Support/MiniCloud Sync/config.json) - Gespeichert werden: Server-URL, Username, Sync-Pfade - Passwort wird im OS-Keychain gespeichert (Windows Credential Manager, macOS Keychain, Linux Secret Service) - nicht in der Config-Datei Auto-Login: - Beim Start wird gespeicherte Config geladen - Wenn Credentials im Keychain vorhanden: automatischer Login - Wenn Sync-Pfade konfiguriert: Sync startet sofort automatisch - Bei Fehler: Login-Screen mit vorausgefuellten Feldern Config ueberlebt Updates: - Config liegt ausserhalb des Installationsverzeichnisses - NSIS-Installer ueberschreibt nur App-Dateien, nicht AppData - installMode: "both" erlaubt per-User und per-Machine Installation Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/src/lib.rs | 65 +++++++++++++++++++- clients/desktop/src-tauri/src/sync/config.rs | 61 ++++++++++++++++++ clients/desktop/src-tauri/src/sync/mod.rs | 1 + clients/desktop/src-tauri/tauri.conf.json | 4 +- clients/desktop/src/App.vue | 29 +++++++++ 5 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 clients/desktop/src-tauri/src/sync/config.rs diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index 9ea94bc..7d913a8 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -10,6 +10,7 @@ use tauri::{ }; use sync::api::MiniCloudApi; +use sync::config::AppConfig; use sync::engine::{SyncEngine, SyncMode, SyncPath}; use sync::watcher::{FileWatcher, ChangeKind}; @@ -36,7 +37,57 @@ async fn login( let result = api.login(&username, &password).await?; *state.api.lock().unwrap() = Some(api); - *state.username.lock().unwrap() = Some(username); + *state.username.lock().unwrap() = Some(username.clone()); + + // Save credentials + let _ = AppConfig::save_password(&server_url, &username, &password); + let mut config = AppConfig::load(); + config.server_url = server_url; + config.username = username; + let _ = config.save(); + + Ok(serde_json::json!({ + "username": result.user.username, + "role": result.user.role, + })) +} + +// --- Saved Config --- + +#[tauri::command] +fn load_saved_config() -> Result { + let config = AppConfig::load(); + let has_credentials = if !config.server_url.is_empty() && !config.username.is_empty() { + AppConfig::load_password(&config.server_url, &config.username).is_some() + } else { + false + }; + + Ok(serde_json::json!({ + "server_url": config.server_url, + "username": config.username, + "sync_paths": config.sync_paths, + "has_credentials": has_credentials, + "has_config": !config.server_url.is_empty(), + })) +} + +#[tauri::command] +async fn auto_login(state: State<'_, AppState>) -> Result { + let config = AppConfig::load(); + if config.server_url.is_empty() || config.username.is_empty() { + return Err("Keine gespeicherten Zugangsdaten".to_string()); + } + + let password = AppConfig::load_password(&config.server_url, &config.username) + .ok_or("Passwort nicht im Keychain gefunden")?; + + let mut api = MiniCloudApi::new(&config.server_url); + let result = api.login(&config.username, &password).await?; + + *state.api.lock().unwrap() = Some(api); + *state.username.lock().unwrap() = Some(config.username.clone()); + *state.sync_paths.lock().unwrap() = config.sync_paths; Ok(serde_json::json!({ "username": result.user.username, @@ -71,12 +122,22 @@ fn add_sync_path( state.sync_paths.lock().unwrap().push(sp.clone()); + // Save to config + let mut config = AppConfig::load(); + config.sync_paths = state.sync_paths.lock().unwrap().clone(); + let _ = config.save(); + Ok(serde_json::to_value(sp).map_err(|e| e.to_string())?) } #[tauri::command] fn remove_sync_path(state: State<'_, AppState>, id: String) -> Result { state.sync_paths.lock().unwrap().retain(|p| p.id != id); + + let mut config = AppConfig::load(); + config.sync_paths = state.sync_paths.lock().unwrap().clone(); + let _ = config.save(); + Ok("Sync-Pfad entfernt".to_string()) } @@ -544,6 +605,8 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ + load_saved_config, + auto_login, login, add_sync_path, remove_sync_path, diff --git a/clients/desktop/src-tauri/src/sync/config.rs b/clients/desktop/src-tauri/src/sync/config.rs new file mode 100644 index 0000000..4b8f1ed --- /dev/null +++ b/clients/desktop/src-tauri/src/sync/config.rs @@ -0,0 +1,61 @@ +use crate::sync::engine::SyncPath; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AppConfig { + pub server_url: String, + pub username: String, + pub sync_paths: Vec, + #[serde(default)] + pub auto_start: bool, +} + +impl AppConfig { + /// Get the config file path (OS-specific app data directory) + fn config_path() -> PathBuf { + let config_dir = dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("MiniCloud Sync"); + std::fs::create_dir_all(&config_dir).ok(); + config_dir.join("config.json") + } + + /// Load config from disk + pub fn load() -> Self { + let path = Self::config_path(); + if path.exists() { + match std::fs::read_to_string(&path) { + Ok(content) => { + match serde_json::from_str(&content) { + Ok(config) => return config, + Err(e) => eprintln!("[Config] Parse-Fehler: {}", e), + } + } + Err(e) => eprintln!("[Config] Lese-Fehler: {}", e), + } + } + Self::default() + } + + /// Save config to disk + pub fn save(&self) -> Result<(), String> { + let path = Self::config_path(); + let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?; + std::fs::write(&path, json).map_err(|e| format!("Config speichern: {}", e)) + } + + /// Save password to OS keychain + pub fn save_password(server_url: &str, username: &str, password: &str) -> Result<(), String> { + let service = format!("minicloud-sync-{}", server_url); + let entry = keyring::Entry::new(&service, username).map_err(|e| e.to_string())?; + entry.set_password(password).map_err(|e| format!("Keychain: {}", e)) + } + + /// Load password from OS keychain + pub fn load_password(server_url: &str, username: &str) -> Option { + let service = format!("minicloud-sync-{}", server_url); + let entry = keyring::Entry::new(&service, username).ok()?; + entry.get_password().ok() + } +} diff --git a/clients/desktop/src-tauri/src/sync/mod.rs b/clients/desktop/src-tauri/src/sync/mod.rs index 014f7fc..9bfc28f 100644 --- a/clients/desktop/src-tauri/src/sync/mod.rs +++ b/clients/desktop/src-tauri/src/sync/mod.rs @@ -1,3 +1,4 @@ pub mod api; +pub mod config; pub mod engine; pub mod watcher; diff --git a/clients/desktop/src-tauri/tauri.conf.json b/clients/desktop/src-tauri/tauri.conf.json index c51aa6b..9d5274b 100644 --- a/clients/desktop/src-tauri/tauri.conf.json +++ b/clients/desktop/src-tauri/tauri.conf.json @@ -44,7 +44,9 @@ "nsis": { "installerIcon": "icons/icon.ico", "headerImage": null, - "sidebarImage": null + "sidebarImage": null, + "installMode": "both", + "displayLanguageSelector": false } } } diff --git a/clients/desktop/src/App.vue b/clients/desktop/src/App.vue index b8542b4..1027edb 100644 --- a/clients/desktop/src/App.vue +++ b/clients/desktop/src/App.vue @@ -223,6 +223,35 @@ function formatSize(b) { } onMounted(async () => { + // Try auto-login with saved credentials + try { + const saved = await invoke("load_saved_config"); + if (saved.has_credentials) { + loginLoading.value = true; + serverUrl.value = saved.server_url; + username.value = saved.username; + try { + const result = await invoke("auto_login"); + userInfo.value = result; + screen.value = "main"; + syncStatus.value = `Verbunden als ${result.username}`; + syncPaths.value = (await invoke("get_sync_paths")); + await loadFileTree(); + // Auto-start sync if paths configured + if (syncPaths.value.length > 0) { + await startSync(); + } + } catch (err) { + syncStatus.value = "Auto-Login fehlgeschlagen"; + // Show login screen with pre-filled fields + } + loginLoading.value = false; + } else if (saved.has_config) { + serverUrl.value = saved.server_url; + username.value = saved.username; + } + } catch { /* no saved config */ } + unlistenStatus = await listen("sync-status", e => { syncing.value = e.payload === "syncing"; syncStatus.value = e.payload === "syncing" ? "Synchronisiere..." : "Synchronisiert";