refined client configuration manager flow

This commit is contained in:
2026-01-03 22:05:30 +08:00
parent f85d7e773d
commit afdff29637
3 changed files with 99 additions and 40 deletions

View File

@@ -365,8 +365,7 @@ fn save_client_config(config: AppConfig) -> Result<(), String> {
#[tauri::command] #[tauri::command]
fn open_client_config_manager() -> Result<(), String> { fn open_client_config_manager() -> Result<(), String> {
open_config_manager_window(); open_config_manager_window().map_err(|e| e.to_string())
Ok(())
} }
#[tauri::command] #[tauri::command]

View File

@@ -28,6 +28,10 @@ pub enum ClientConfigError {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("failed to parse client config: {0}")] #[error("failed to parse client config: {0}")]
Parse(#[from] serde_json::Error), 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"; 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() value.trim_end_matches('/').to_string()
} }
fn parse_http_url(value: &str) -> Option<String> {
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 { fn sanitize(mut config: AppConfig) -> AppConfig {
// Trim and normalize optional api_base_url
config.api_base_url = config config.api_base_url = config
.api_base_url .api_base_url
.and_then(|v| { .and_then(|v| parse_http_url(v.trim()))
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
}
}
})
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string())) .or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
.map(|v| strip_trailing_slash(&v)); .map(|v| strip_trailing_slash(&v));
let auth_url_trimmed = config.auth.auth_url.trim(); let auth_url_trimmed = config.auth.auth_url.trim();
if auth_url_trimmed.is_empty() { config.auth.auth_url = parse_http_url(auth_url_trimmed)
config.auth.auth_url = DEFAULT_AUTH_URL.to_string(); .unwrap_or_else(|| 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();
}
if config.auth.audience.trim().is_empty() { if config.auth.audience.trim().is_empty() {
config.auth.audience = DEFAULT_JWT_AUDIENCE.to_string(); config.auth.audience = DEFAULT_JWT_AUDIENCE.to_string();
@@ -139,23 +139,28 @@ pub fn save_app_config(config: AppConfig) -> Result<AppConfig, ClientConfigError
let sanitized = sanitize(config); let sanitized = sanitize(config);
let serialized = serde_json::to_string_pretty(&sanitized)?; let serialized = serde_json::to_string_pretty(&sanitized)?;
fs::write(&path, serialized)?;
let temp_path = path.with_extension("tmp");
fs::write(&temp_path, serialized)?;
fs::rename(&temp_path, &path)?;
Ok(sanitized) Ok(sanitized)
} }
pub fn open_config_manager_window() { pub fn open_config_manager_window() -> Result<(), ClientConfigError> {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL); let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL);
if let Some(window) = existing_webview_window { if let Some(window) = existing_webview_window {
if let Err(e) = window.show() { if let Err(e) = window.show() {
error!("Failed to show client config manager window: {e}"); error!("Failed to show client config manager window: {e}");
return Err(ClientConfigError::ShowWindow(e));
} }
return; return Ok(());
} }
info!("Starting client config manager..."); info!("Starting client config manager...");
let webview_window = match tauri::WebviewWindowBuilder::new( match tauri::WebviewWindowBuilder::new(
app_handle, app_handle,
CLIENT_CONFIG_MANAGER_WINDOW_LABEL, CLIENT_CONFIG_MANAGER_WINDOW_LABEL,
tauri::WebviewUrl::App("/client-config-manager".into()), tauri::WebviewUrl::App("/client-config-manager".into()),
@@ -164,15 +169,14 @@ pub fn open_config_manager_window() {
.inner_size(600.0, 500.0) .inner_size(600.0, 500.0)
.build() .build()
{ {
Ok(window) => { Ok(_) => {
info!("Client config manager window builder succeeded"); info!("Client config manager window builder succeeded");
window info!("Client config manager window initialized successfully.");
Ok(())
} }
Err(e) => { Err(e) => {
error!("Failed to build client config manager window: {}", e); error!("Failed to build client config manager window: {}", e);
return; Err(ClientConfigError::Window(e))
} }
}; }
info!("Client config manager window initialized successfully.");
} }

View File

@@ -20,6 +20,7 @@
let saving = false; let saving = false;
let errorMessage = ""; let errorMessage = "";
let successMessage = ""; let successMessage = "";
let restartError = "";
const loadConfig = async () => { const loadConfig = async () => {
try { 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 () => { const save = async () => {
if (saving) return; if (saving) return;
errorMessage = validate();
if (errorMessage) return;
saving = true; saving = true;
errorMessage = ""; errorMessage = "";
successMessage = ""; successMessage = "";
restartError = "";
try { try {
await invoke("save_client_config", { await invoke("save_client_config", {
config: { config: {
@@ -52,8 +93,7 @@
}, },
}); });
successMessage = "Configuration saved. Please restart the app."; successMessage = "Configuration saved. Restart to apply changes.";
await invoke("restart_app");
} catch (err) { } catch (err) {
errorMessage = `Failed to save config: ${err}`; errorMessage = `Failed to save config: ${err}`;
} finally { } finally {
@@ -61,6 +101,15 @@
} }
}; };
const restart = async () => {
restartError = "";
try {
await invoke("restart_app");
} catch (err) {
restartError = `Restart failed: ${err}`;
}
};
onMount(loadConfig); onMount(loadConfig);
</script> </script>
@@ -95,7 +144,10 @@
{#if successMessage} {#if successMessage}
<p class="text-sm text-success">{successMessage}</p> <p class="text-sm text-success">{successMessage}</p>
{/if} {/if}
{#if restartError}
<p class="text-sm text-error">{restartError}</p>
{/if}
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button <button
class="btn" class="btn"
@@ -105,5 +157,9 @@
> >
{saving ? "Saving..." : "Save"} {saving ? "Saving..." : "Save"}
</button> </button>
<button class="btn btn-outline" on:click={restart}>
Restart app
</button>
</div> </div>
</div> </div>