Allow desktop app to connect to selfhosted servers (#4902)

# Description of Changes
Changes the desktop app to allow connections to self-hosted servers on
first startup. This was quite involved and hit loads of CORS issues all
through the stack, but I think it's working now. This also changes the
bundled backend to spawn on an OS-decided port rather than always
spawning on `8080`, which means that the user can have other things
running on port `8080` now and the app will still work fine. There were
quite a few places that needed to be updated to decouple the app from
explicitly using `8080` and I was originally going to split those
changes out into another PR (#4939), but I couldn't get it working
independently in the time I had, so the diff here is just going to be
complex and contian two distinct changes - sorry 🙁
This commit is contained in:
James Brunton
2025-11-20 10:03:34 +00:00
committed by GitHub
parent 75414b89f9
commit f4725b98b0
43 changed files with 3209 additions and 218 deletions
@@ -0,0 +1,111 @@
use crate::state::connection_state::{
AppConnectionState,
ConnectionMode,
ServerConfig,
};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tauri_plugin_store::StoreExt;
const STORE_FILE: &str = "connection.json";
const FIRST_LAUNCH_KEY: &str = "setup_completed";
const CONNECTION_MODE_KEY: &str = "connection_mode";
const SERVER_CONFIG_KEY: &str = "server_config";
#[derive(Debug, Serialize, Deserialize)]
pub struct ConnectionConfig {
pub mode: ConnectionMode,
pub server_config: Option<ServerConfig>,
}
#[tauri::command]
pub async fn get_connection_config(
app_handle: AppHandle,
state: State<'_, AppConnectionState>,
) -> Result<ConnectionConfig, String> {
// Try to load from store
let store = app_handle
.store(STORE_FILE)
.map_err(|e| format!("Failed to access store: {}", e))?;
let mode = store
.get(CONNECTION_MODE_KEY)
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or(ConnectionMode::Offline);
let server_config: Option<ServerConfig> = store
.get(SERVER_CONFIG_KEY)
.and_then(|v| serde_json::from_value(v.clone()).ok());
// Update in-memory state
if let Ok(mut conn_state) = state.0.lock() {
conn_state.mode = mode.clone();
conn_state.server_config = server_config.clone();
}
Ok(ConnectionConfig {
mode,
server_config,
})
}
#[tauri::command]
pub async fn set_connection_mode(
app_handle: AppHandle,
state: State<'_, AppConnectionState>,
mode: ConnectionMode,
server_config: Option<ServerConfig>,
) -> Result<(), String> {
log::info!("Setting connection mode: {:?}", mode);
// Update in-memory state
if let Ok(mut conn_state) = state.0.lock() {
conn_state.mode = mode.clone();
conn_state.server_config = server_config.clone();
}
// Save to store
let store = app_handle
.store(STORE_FILE)
.map_err(|e| format!("Failed to access store: {}", e))?;
store.set(
CONNECTION_MODE_KEY,
serde_json::to_value(&mode).map_err(|e| format!("Failed to serialize mode: {}", e))?,
);
if let Some(config) = &server_config {
store.set(
SERVER_CONFIG_KEY,
serde_json::to_value(config)
.map_err(|e| format!("Failed to serialize config: {}", e))?,
);
} else {
store.delete(SERVER_CONFIG_KEY);
}
// Mark setup as completed
store.set(FIRST_LAUNCH_KEY, serde_json::json!(true));
store
.save()
.map_err(|e| format!("Failed to save store: {}", e))?;
log::info!("Connection mode saved successfully");
Ok(())
}
#[tauri::command]
pub async fn is_first_launch(app_handle: AppHandle) -> Result<bool, String> {
let store = app_handle
.store(STORE_FILE)
.map_err(|e| format!("Failed to access store: {}", e))?;
let setup_completed = store
.get(FIRST_LAUNCH_KEY)
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(!setup_completed)
}