//! Tauri commands for pairing with a server (M10.7a). //! //! Linking is opt-in and reversible; the app is fully usable having never touched //! any of this. The Settings UI (M10.7e) drives these. use serde::{Deserialize, Serialize}; use tauri::State; use crate::local::Db; use crate::sync::client::{self, Identity, ProbeResult}; use crate::sync::compat::Compatibility; use crate::sync::state; /// Ask a server who it is, without committing to anything. The UI calls this as the /// user finishes typing an address, so they see what answered before handing over /// credentials. #[tauri::command] pub async fn sync_probe(url: String) -> Result { client::probe(&url).await } /// Either a password login or a token pasted from the web app. Both are offered /// because neither covers everyone: a fresh install has no session to mint a token /// from, while someone using a password manager or SSO may prefer not to type a /// password into a desktop app at all. #[derive(Deserialize)] pub struct LinkInput { pub url: String, #[serde(default)] pub email: Option, #[serde(default)] pub password: Option, #[serde(default)] pub token: Option, /// How this device is labelled in the server's device list. #[serde(default)] pub name: Option, } #[derive(Serialize)] pub struct LinkResult { pub status: state::Status, pub identity: Identity, /// Carried through so the UI can warn about a `degraded` server right after /// linking, instead of staying silent until a feature quietly does nothing. pub compatibility: Compatibility, } /// A recognizable default, so a server's device list doesn't fill up with "Device". fn default_device_name() -> String { format!("ThoughtSync desktop ({})", std::env::consts::OS) } fn trimmed(value: &Option) -> Option<&str> { value.as_deref().map(str::trim).filter(|s| !s.is_empty()) } #[tauri::command] pub async fn sync_link(input: LinkInput, db: State<'_, Db>) -> Result { // 1. Handshake FIRST. Never hand credentials to a server we've established we // can't sync with — and an incompatible server is exactly the case where a // later failure would be hardest to attribute. let probe = client::probe(&input.url).await?; if let Compatibility::Incompatible { reason, .. } = &probe.compatibility { return Err(reason.clone()); } let base_url = probe.base_url; // 2. Obtain a credential. let (token, identity) = match trimmed(&input.token) { Some(token) => { // Verify before storing: an unverified paste turns a copy/paste slip // into a failure that only surfaces at the next sync. let identity = client::fetch_identity(&base_url, token).await?; (token.to_string(), identity) } None => { let (Some(email), Some(password)) = (trimmed(&input.email), trimmed(&input.password)) else { return Err("Enter your email and password, or paste a device token.".to_string()); }; let name = trimmed(&input.name) .map(str::to_string) .unwrap_or_else(default_device_name); client::device_login(&base_url, email, password, &name).await? } }; // 3. Persist. The lock is taken only now, for two reasons: a std MutexGuard // isn't Send so it cannot be held across an await, and holding the store // locked for a network round-trip would freeze every note operation in the UI. let status = { let conn = db.0.lock().map_err(|e| e.to_string())?; state::set_link(&conn, &base_url, &token).map_err(|e| e.to_string())?; state::status(&conn).map_err(|e| e.to_string())? }; log::info!("linked to {} as {}", base_url, identity.email); Ok(LinkResult { status, identity, compatibility: probe.compatibility, }) } /// Stop syncing and forget the server. /// /// Local only: the device token remains valid on the SERVER until revoked there /// (Account → Linked devices). We can't reliably revoke it from here — a pasted /// token arrives without its device id — so the UI must say so rather than imply a /// remote revoke that didn't happen. Tracked for follow-up. #[tauri::command] pub fn sync_unlink(db: State<'_, Db>) -> Result { let conn = db.0.lock().map_err(|e| e.to_string())?; state::clear_link(&conn).map_err(|e| e.to_string())?; log::info!("unlinked from server"); state::status(&conn).map_err(|e| e.to_string()) } #[tauri::command] pub fn sync_status(db: State<'_, Db>) -> Result { let conn = db.0.lock().map_err(|e| e.to_string())?; state::status(&conn).map_err(|e| e.to_string()) }