max / audiofiles
4 files changed,
+100 insertions,
-63 deletions
| @@ -671,6 +671,12 @@ dependencies = [ | |||
| 671 | 671 | ] | |
| 672 | 672 | ||
| 673 | 673 | [[package]] | |
| 674 | + | name = "byteorder" | |
| 675 | + | version = "1.5.0" | |
| 676 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 677 | + | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | |
| 678 | + | ||
| 679 | + | [[package]] | |
| 674 | 680 | name = "byteorder-lite" | |
| 675 | 681 | version = "0.1.0" | |
| 676 | 682 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2598,7 +2604,12 @@ version = "3.6.3" | |||
| 2598 | 2604 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2599 | 2605 | checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" | |
| 2600 | 2606 | dependencies = [ | |
| 2607 | + | "byteorder", | |
| 2608 | + | "linux-keyutils", | |
| 2601 | 2609 | "log", | |
| 2610 | + | "security-framework 2.11.1", | |
| 2611 | + | "security-framework 3.7.0", | |
| 2612 | + | "windows-sys 0.60.2", | |
| 2602 | 2613 | "zeroize", | |
| 2603 | 2614 | ] | |
| 2604 | 2615 | ||
| @@ -2723,6 +2734,16 @@ dependencies = [ | |||
| 2723 | 2734 | ] | |
| 2724 | 2735 | ||
| 2725 | 2736 | [[package]] | |
| 2737 | + | name = "linux-keyutils" | |
| 2738 | + | version = "0.2.5" | |
| 2739 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2740 | + | checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" | |
| 2741 | + | dependencies = [ | |
| 2742 | + | "bitflags 2.11.0", | |
| 2743 | + | "libc", | |
| 2744 | + | ] | |
| 2745 | + | ||
| 2746 | + | [[package]] | |
| 2726 | 2747 | name = "linux-raw-sys" | |
| 2727 | 2748 | version = "0.4.15" | |
| 2728 | 2749 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2942,7 +2963,7 @@ dependencies = [ | |||
| 2942 | 2963 | "openssl-probe", | |
| 2943 | 2964 | "openssl-sys", | |
| 2944 | 2965 | "schannel", | |
| 2945 | - | "security-framework", | |
| 2966 | + | "security-framework 3.7.0", | |
| 2946 | 2967 | "security-framework-sys", | |
| 2947 | 2968 | "tempfile", | |
| 2948 | 2969 | ] | |
| @@ -4297,9 +4318,9 @@ dependencies = [ | |||
| 4297 | 4318 | ||
| 4298 | 4319 | [[package]] | |
| 4299 | 4320 | name = "rustls-webpki" | |
| 4300 | - | version = "0.103.11" | |
| 4321 | + | version = "0.103.13" | |
| 4301 | 4322 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4302 | - | checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" | |
| 4323 | + | checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" | |
| 4303 | 4324 | dependencies = [ | |
| 4304 | 4325 | "ring", | |
| 4305 | 4326 | "rustls-pki-types", | |
| @@ -4350,6 +4371,19 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | |||
| 4350 | 4371 | ||
| 4351 | 4372 | [[package]] | |
| 4352 | 4373 | name = "security-framework" | |
| 4374 | + | version = "2.11.1" | |
| 4375 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4376 | + | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" | |
| 4377 | + | dependencies = [ | |
| 4378 | + | "bitflags 2.11.0", | |
| 4379 | + | "core-foundation 0.9.4", | |
| 4380 | + | "core-foundation-sys", | |
| 4381 | + | "libc", | |
| 4382 | + | "security-framework-sys", | |
| 4383 | + | ] | |
| 4384 | + | ||
| 4385 | + | [[package]] | |
| 4386 | + | name = "security-framework" | |
| 4353 | 4387 | version = "3.7.0" | |
| 4354 | 4388 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4355 | 4389 | checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | //! Sync settings panel: egui Window overlay with 4 states matching the SyncKit flow. | |
| 2 | 2 | ||
| 3 | 3 | use egui; | |
| 4 | - | use tracing::{debug, error, warn}; | |
| 4 | + | use tracing::{error, warn}; | |
| 5 | 5 | ||
| 6 | 6 | use audiofiles_sync::{SyncManager, SyncState, SyncStatus, TierInfo}; | |
| 7 | 7 | ||
| @@ -239,19 +239,13 @@ fn draw_disconnected( | |||
| 239 | 239 | ui.add_space(12.0); | |
| 240 | 240 | if ui.button("Connect").clicked() { | |
| 241 | 241 | match sync.start_auth() { | |
| 242 | - | Ok(session) => { | |
| 243 | - | // Try to open browser | |
| 244 | - | // Open browser (macOS: open, Linux: xdg-open, Windows: start) | |
| 242 | + | Ok(auth_url) => { | |
| 245 | 243 | #[cfg(target_os = "macos")] | |
| 246 | - | let _ = std::process::Command::new("open").arg(&session.auth_url).spawn(); | |
| 244 | + | let _ = std::process::Command::new("open").arg(&auth_url).spawn(); | |
| 247 | 245 | #[cfg(target_os = "linux")] | |
| 248 | - | let _ = std::process::Command::new("xdg-open").arg(&session.auth_url).spawn(); | |
| 246 | + | let _ = std::process::Command::new("xdg-open").arg(&auth_url).spawn(); | |
| 249 | 247 | #[cfg(target_os = "windows")] | |
| 250 | - | let _ = std::process::Command::new("cmd").args(["/c", "start", &session.auth_url]).spawn(); | |
| 251 | - | // Store session for later — we need it when completing auth. | |
| 252 | - | // For simplicity, we don't store the full session object here; | |
| 253 | - | // the manual code entry path handles the case where the callback | |
| 254 | - | // doesn't fire. | |
| 248 | + | let _ = std::process::Command::new("cmd").args(["/c", "start", &auth_url]).spawn(); | |
| 255 | 249 | state.sync.auth_code_input.clear(); | |
| 256 | 250 | } | |
| 257 | 251 | Err(e) => { | |
| @@ -264,25 +258,18 @@ fn draw_disconnected( | |||
| 264 | 258 | /// Authenticating state: waiting for OAuth callback. | |
| 265 | 259 | fn draw_authenticating( | |
| 266 | 260 | ui: &mut egui::Ui, | |
| 267 | - | state: &mut BrowserState, | |
| 261 | + | _state: &mut BrowserState, | |
| 268 | 262 | _sync: &SyncManager, | |
| 269 | 263 | ) { | |
| 270 | - | ui.label("Opening browser for authentication..."); | |
| 264 | + | ui.label("Waiting for authentication in your browser..."); | |
| 271 | 265 | ui.add_space(8.0); | |
| 272 | 266 | ui.spinner(); | |
| 273 | - | ui.add_space(12.0); | |
| 274 | - | ||
| 275 | - | // Manual code entry fallback | |
| 276 | - | ui.collapsing("Enter code manually", |ui| { | |
| 277 | - | ui.horizontal(|ui| { | |
| 278 | - | ui.text_edit_singleline(&mut state.sync.auth_code_input); | |
| 279 | - | if ui.button("Submit").clicked() && !state.sync.auth_code_input.is_empty() { | |
| 280 | - | // Manual code entry would need the AuthSession stored somewhere. | |
| 281 | - | // For now, this is a placeholder — the primary flow uses the callback server. | |
| 282 | - | debug!("Manual code entry: {}", state.sync.auth_code_input); | |
| 283 | - | } | |
| 284 | - | }); | |
| 285 | - | }); | |
| 267 | + | ui.add_space(8.0); | |
| 268 | + | ui.label( | |
| 269 | + | egui::RichText::new("The app will update automatically once you sign in.") | |
| 270 | + | .small() | |
| 271 | + | .weak(), | |
| 272 | + | ); | |
| 286 | 273 | } | |
| 287 | 274 | ||
| 288 | 275 | /// NeedsEncryption state: password setup. |
| @@ -128,7 +128,7 @@ pub fn start_auth(client: &SyncKitClient) -> Result<AuthSession> { | |||
| 128 | 128 | tracing::warn!("OAuth callback state mismatch — rejecting"); | |
| 129 | 129 | let body = "<html><body><h1>Authentication failed</h1><p>CSRF state mismatch. Please try again.</p></body></html>"; | |
| 130 | 130 | let response = format!( | |
| 131 | - | "HTTP/1.1 403 Forbidden\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", | |
| 131 | + | "HTTP/1.1 403 Forbidden\r\nContent-Type: text/html\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n{}", | |
| 132 | 132 | body.len(), | |
| 133 | 133 | body | |
| 134 | 134 | ); | |
| @@ -138,7 +138,7 @@ pub fn start_auth(client: &SyncKitClient) -> Result<AuthSession> { | |||
| 138 | 138 | ||
| 139 | 139 | let body = "<html><body><h1>Authentication successful</h1><p>You can close this tab and return to audiofiles.</p></body></html>"; | |
| 140 | 140 | let response = format!( | |
| 141 | - | "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", | |
| 141 | + | "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n{}", | |
| 142 | 142 | body.len(), | |
| 143 | 143 | body | |
| 144 | 144 | ); |
| @@ -18,7 +18,6 @@ use synckit_client::SyncKitClient; | |||
| 18 | 18 | use tokio::runtime::Handle; | |
| 19 | 19 | use tokio::sync::mpsc; | |
| 20 | 20 | ||
| 21 | - | use auth::AuthSession; | |
| 22 | 21 | use error::Result; | |
| 23 | 22 | use scheduler::SyncCommand; | |
| 24 | 23 | ||
| @@ -112,54 +111,71 @@ impl SyncManager { | |||
| 112 | 111 | } | |
| 113 | 112 | ||
| 114 | 113 | /// Start the OAuth2 PKCE authentication flow. | |
| 115 | - | /// Returns an `AuthSession` with the auth URL to open in a browser. | |
| 114 | + | /// Opens the callback server and returns the auth URL. A background task | |
| 115 | + | /// automatically awaits the callback and completes authentication. | |
| 116 | 116 | #[instrument(skip_all)] | |
| 117 | - | pub fn start_auth(&self) -> Result<AuthSession> { | |
| 117 | + | pub fn start_auth(&self) -> Result<String> { | |
| 118 | 118 | self.status.lock().state = SyncState::Authenticating; | |
| 119 | - | auth::start_auth(&self.client) | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | /// Complete authentication after the OAuth callback is received. | |
| 123 | - | #[instrument(skip_all)] | |
| 124 | - | pub fn complete_auth(&self, code: String, state: String, session: &AuthSession) { | |
| 125 | - | if state != session.expected_state { | |
| 126 | - | let mut s = self.status.lock(); | |
| 127 | - | s.last_error = Some("CSRF state mismatch".to_string()); | |
| 128 | - | s.state = SyncState::Disconnected; | |
| 129 | - | return; | |
| 130 | - | } | |
| 119 | + | let session = auth::start_auth(&self.client)?; | |
| 120 | + | let auth_url = session.auth_url.clone(); | |
| 131 | 121 | ||
| 122 | + | // Spawn background task to await callback and auto-complete auth | |
| 132 | 123 | let client = self.client.clone(); | |
| 133 | - | let port = session.port; | |
| 134 | - | let verifier = session.code_verifier.clone(); | |
| 135 | 124 | let status = self.status.clone(); | |
| 136 | 125 | let db_path = self.db_path.clone(); | |
| 126 | + | let expected_state = session.expected_state.clone(); | |
| 127 | + | let code_verifier = session.code_verifier.clone(); | |
| 128 | + | let port = session.port; | |
| 137 | 129 | ||
| 138 | 130 | self.runtime.spawn(async move { | |
| 139 | - | match client.authenticate_with_code(&code, &verifier, port).await { | |
| 140 | - | Ok(_) => { | |
| 141 | - | // Try to load key from keychain (may already exist on this device) | |
| 142 | - | let has_key = client.try_load_key_from_keychain().unwrap_or(false); | |
| 143 | - | if has_key { | |
| 131 | + | match session.code_rx.await { | |
| 132 | + | Ok(result) => { | |
| 133 | + | if result.state != expected_state { | |
| 144 | 134 | let mut s = status.lock(); | |
| 145 | - | s.state = SyncState::Ready; | |
| 146 | - | load_sync_settings_into_status(&db_path, &mut s); | |
| 147 | - | } else { | |
| 148 | - | // Check if server has an encrypted key | |
| 149 | - | let has_server_key = client.has_server_key().await.unwrap_or(false); | |
| 150 | - | status.lock().state = | |
| 151 | - | SyncState::NeedsEncryption { has_server_key }; | |
| 135 | + | s.last_error = Some("CSRF state mismatch".to_string()); | |
| 136 | + | s.state = SyncState::Disconnected; | |
| 137 | + | return; | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | match client | |
| 141 | + | .authenticate_with_code(&result.code, &code_verifier, port) | |
| 142 | + | .await | |
| 143 | + | { | |
| 144 | + | Ok(_) => { | |
| 145 | + | let has_key = client.try_load_key_from_keychain().unwrap_or(false); | |
| 146 | + | if has_key { | |
| 147 | + | let mut s = status.lock(); | |
| 148 | + | s.state = SyncState::Ready; | |
| 149 | + | load_sync_settings_into_status(&db_path, &mut s); | |
| 150 | + | } else { | |
| 151 | + | let has_server_key = | |
| 152 | + | client.has_server_key().await.unwrap_or(false); | |
| 153 | + | status.lock().state = | |
| 154 | + | SyncState::NeedsEncryption { has_server_key }; | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | Err(e) => { | |
| 158 | + | let mut s = status.lock(); | |
| 159 | + | s.state = SyncState::Disconnected; | |
| 160 | + | s.last_error = Some(format!("Auth failed: {e}")); | |
| 161 | + | } | |
| 152 | 162 | } | |
| 153 | 163 | } | |
| 154 | - | Err(e) => { | |
| 164 | + | Err(_) => { | |
| 165 | + | // Callback server timed out or was dropped | |
| 155 | 166 | let mut s = status.lock(); | |
| 156 | - | s.state = SyncState::Disconnected; | |
| 157 | - | s.last_error = Some(format!("Auth failed: {e}")); | |
| 167 | + | if s.state == SyncState::Authenticating { | |
| 168 | + | s.state = SyncState::Disconnected; | |
| 169 | + | s.last_error = Some("Authentication timed out".to_string()); | |
| 170 | + | } | |
| 158 | 171 | } | |
| 159 | 172 | } | |
| 160 | 173 | }); | |
| 174 | + | ||
| 175 | + | Ok(auth_url) | |
| 161 | 176 | } | |
| 162 | 177 | ||
| 178 | + | ||
| 163 | 179 | /// Set up encryption (new or existing, depending on `is_new`). | |
| 164 | 180 | #[instrument(skip_all)] | |
| 165 | 181 | pub fn setup_encryption(&self, password: String, is_new: bool) { |