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) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use sync::api::MiniCloudApi;
|
use sync::api::MiniCloudApi;
|
||||||
|
use sync::config::AppConfig;
|
||||||
use sync::engine::{SyncEngine, SyncMode, SyncPath};
|
use sync::engine::{SyncEngine, SyncMode, SyncPath};
|
||||||
use sync::watcher::{FileWatcher, ChangeKind};
|
use sync::watcher::{FileWatcher, ChangeKind};
|
||||||
|
|
||||||
@@ -36,7 +37,57 @@ async fn login(
|
|||||||
let result = api.login(&username, &password).await?;
|
let result = api.login(&username, &password).await?;
|
||||||
|
|
||||||
*state.api.lock().unwrap() = Some(api);
|
*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<serde_json::Value, String> {
|
||||||
|
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<serde_json::Value, String> {
|
||||||
|
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!({
|
Ok(serde_json::json!({
|
||||||
"username": result.user.username,
|
"username": result.user.username,
|
||||||
@@ -71,12 +122,22 @@ fn add_sync_path(
|
|||||||
|
|
||||||
state.sync_paths.lock().unwrap().push(sp.clone());
|
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())?)
|
Ok(serde_json::to_value(sp).map_err(|e| e.to_string())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn remove_sync_path(state: State<'_, AppState>, id: String) -> Result<String, String> {
|
fn remove_sync_path(state: State<'_, AppState>, id: String) -> Result<String, String> {
|
||||||
state.sync_paths.lock().unwrap().retain(|p| p.id != id);
|
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())
|
Ok("Sync-Pfad entfernt".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +605,8 @@ pub fn run() {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
load_saved_config,
|
||||||
|
auto_login,
|
||||||
login,
|
login,
|
||||||
add_sync_path,
|
add_sync_path,
|
||||||
remove_sync_path,
|
remove_sync_path,
|
||||||
|
|||||||
@@ -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<SyncPath>,
|
||||||
|
#[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<String> {
|
||||||
|
let service = format!("minicloud-sync-{}", server_url);
|
||||||
|
let entry = keyring::Entry::new(&service, username).ok()?;
|
||||||
|
entry.get_password().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
|
pub mod config;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod watcher;
|
pub mod watcher;
|
||||||
|
|||||||
@@ -44,7 +44,9 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"installerIcon": "icons/icon.ico",
|
"installerIcon": "icons/icon.ico",
|
||||||
"headerImage": null,
|
"headerImage": null,
|
||||||
"sidebarImage": null
|
"sidebarImage": null,
|
||||||
|
"installMode": "both",
|
||||||
|
"displayLanguageSelector": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,35 @@ function formatSize(b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
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 => {
|
unlistenStatus = await listen("sync-status", e => {
|
||||||
syncing.value = e.payload === "syncing";
|
syncing.value = e.payload === "syncing";
|
||||||
syncStatus.value = e.payload === "syncing" ? "Synchronisiere..." : "Synchronisiert";
|
syncStatus.value = e.payload === "syncing" ? "Synchronisiere..." : "Synchronisiert";
|
||||||
|
|||||||
Reference in New Issue
Block a user