diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b54bffc..43de6b4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -365,8 +365,7 @@ fn save_client_config(config: AppConfig) -> Result<(), String> { #[tauri::command] fn open_client_config_manager() -> Result<(), String> { - open_config_manager_window(); - Ok(()) + open_config_manager_window().map_err(|e| e.to_string()) } #[tauri::command] diff --git a/src-tauri/src/services/client_config_manager.rs b/src-tauri/src/services/client_config_manager.rs index 2c633e1..ecc6f7e 100644 --- a/src-tauri/src/services/client_config_manager.rs +++ b/src-tauri/src/services/client_config_manager.rs @@ -28,6 +28,10 @@ pub enum ClientConfigError { Io(#[from] std::io::Error), #[error("failed to parse client config: {0}")] Parse(#[from] serde_json::Error), + #[error("failed to build client config manager window: {0}")] + Window(tauri::Error), + #[error("failed to show client config manager window: {0}")] + ShowWindow(tauri::Error), } pub static CLIENT_CONFIG_MANAGER_WINDOW_LABEL: &str = "client_config_manager"; @@ -48,38 +52,34 @@ fn strip_trailing_slash(value: &str) -> String { value.trim_end_matches('/').to_string() } +fn parse_http_url(value: &str) -> Option { + if value.is_empty() { + return None; + } + + let attempts = [value.to_string(), format!("https://{value}")]; + + for attempt in attempts { + if let Ok(parsed) = Url::parse(&attempt) { + if matches!(parsed.scheme(), "http" | "https") { + return Some(strip_trailing_slash(parsed.as_str())); + } + } + } + + None +} + fn sanitize(mut config: AppConfig) -> AppConfig { - // Trim and normalize optional api_base_url config.api_base_url = config .api_base_url - .and_then(|v| { - let trimmed = v.trim().to_string(); - if trimmed.is_empty() { - None - } else { - // ensure scheme present and no double prefixes - if let Ok(parsed) = Url::parse(&trimmed) { - Some(strip_trailing_slash(parsed.as_str())) - } else if let Ok(parsed) = Url::parse(&format!("https://{trimmed}")) { - Some(strip_trailing_slash(parsed.as_str())) - } else { - None - } - } - }) + .and_then(|v| parse_http_url(v.trim())) .or_else(|| Some(DEFAULT_API_BASE_URL.to_string())) .map(|v| strip_trailing_slash(&v)); let auth_url_trimmed = config.auth.auth_url.trim(); - if auth_url_trimmed.is_empty() { - config.auth.auth_url = DEFAULT_AUTH_URL.to_string(); - } else if let Ok(parsed) = Url::parse(auth_url_trimmed) { - config.auth.auth_url = strip_trailing_slash(parsed.as_str()); - } else if let Ok(parsed) = Url::parse(&format!("https://{auth_url_trimmed}")) { - config.auth.auth_url = strip_trailing_slash(parsed.as_str()); - } else { - config.auth.auth_url = DEFAULT_AUTH_URL.to_string(); - } + config.auth.auth_url = parse_http_url(auth_url_trimmed) + .unwrap_or_else(|| DEFAULT_AUTH_URL.to_string()); if config.auth.audience.trim().is_empty() { config.auth.audience = DEFAULT_JWT_AUDIENCE.to_string(); @@ -139,23 +139,28 @@ pub fn save_app_config(config: AppConfig) -> Result Result<(), ClientConfigError> { let app_handle = get_app_handle(); let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL); if let Some(window) = existing_webview_window { if let Err(e) = window.show() { error!("Failed to show client config manager window: {e}"); + return Err(ClientConfigError::ShowWindow(e)); } - return; + return Ok(()); } info!("Starting client config manager..."); - let webview_window = match tauri::WebviewWindowBuilder::new( + match tauri::WebviewWindowBuilder::new( app_handle, CLIENT_CONFIG_MANAGER_WINDOW_LABEL, tauri::WebviewUrl::App("/client-config-manager".into()), @@ -164,15 +169,14 @@ pub fn open_config_manager_window() { .inner_size(600.0, 500.0) .build() { - Ok(window) => { + Ok(_) => { info!("Client config manager window builder succeeded"); - window + info!("Client config manager window initialized successfully."); + Ok(()) } Err(e) => { error!("Failed to build client config manager window: {}", e); - return; + Err(ClientConfigError::Window(e)) } - }; - - info!("Client config manager window initialized successfully."); + } } diff --git a/src/routes/client-config-manager/+page.svelte b/src/routes/client-config-manager/+page.svelte index a30d63d..1a68bce 100644 --- a/src/routes/client-config-manager/+page.svelte +++ b/src/routes/client-config-manager/+page.svelte @@ -20,6 +20,7 @@ let saving = false; let errorMessage = ""; let successMessage = ""; + let restartError = ""; const loadConfig = async () => { try { @@ -36,11 +37,51 @@ } }; + const validate = () => { + if (!form.auth.auth_url.trim()) { + return "Auth URL is required"; + } + + try { + const parsed = new URL(form.auth.auth_url.trim()); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return "Auth URL must start with http or https"; + } + } catch (e) { + return "Auth URL must be a valid URL"; + } + + if (!form.auth.audience.trim()) { + return "JWT audience is required"; + } + + if (form.api_base_url?.trim()) { + try { + const parsed = new URL( + form.api_base_url.trim().startsWith("http") + ? form.api_base_url.trim() + : `https://${form.api_base_url.trim()}` + ); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return "API base URL must start with http or https"; + } + } catch (e) { + return "API base URL must be a valid URL"; + } + } + + return ""; + }; + const save = async () => { if (saving) return; + errorMessage = validate(); + if (errorMessage) return; + saving = true; errorMessage = ""; successMessage = ""; + restartError = ""; try { await invoke("save_client_config", { config: { @@ -52,8 +93,7 @@ }, }); - successMessage = "Configuration saved. Please restart the app."; - await invoke("restart_app"); + successMessage = "Configuration saved. Restart to apply changes."; } catch (err) { errorMessage = `Failed to save config: ${err}`; } finally { @@ -61,6 +101,15 @@ } }; + const restart = async () => { + restartError = ""; + try { + await invoke("restart_app"); + } catch (err) { + restartError = `Restart failed: ${err}`; + } + }; + onMount(loadConfig); @@ -95,7 +144,10 @@ {#if successMessage}

{successMessage}

{/if} - + {#if restartError} +

{restartError}

+ {/if} +
+
+