From d1940e6f28ea1ae867b74c287d0fc86320408fc9 Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 18 Mar 2026 09:23:52 +0100 Subject: [PATCH] feat: add automatic token refresh on 401 responses during profile sync Add RefreshRequest struct for token refresh API calls. Update sync_current_session to detect 401 responses and automatically refresh access tokens using refresh token before retrying profile sync. Store refreshed access and refresh tokens in existing session state. Extract profile URL to variable for reuse in retry logic. --- desktop-client/src-tauri/src/lib.rs | 47 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index 210b266..a7e6fd2 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -75,6 +75,11 @@ struct LoginRequest<'a> { password: &'a str, } +#[derive(Debug, Serialize)] +struct RefreshRequest<'a> { + refresh_token: &'a str, +} + #[derive(Debug, Deserialize)] struct LoginResponse { #[serde(rename = "access_token")] @@ -406,7 +411,7 @@ fn current_metrics(app: &AppHandle) -> Result { } async fn sync_current_session(app: &AppHandle) -> Result { - let existing = { + let mut existing = { let state = app.state::(); let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?; session.clone().ok_or_else(|| "No enrolled profile is available yet".to_string())? @@ -417,13 +422,49 @@ async fn sync_current_session(app: &AppHandle) -> Result { .build() .map_err(|err| err.to_string())?; - let response = client - .get(format!("{}/api/v1/me/profile", existing.server_url.trim_end_matches('/'))) + let profile_url = format!("{}/api/v1/me/profile", existing.server_url.trim_end_matches('/')); + let mut response = client + .get(&profile_url) .bearer_auth(&existing.access_token) .send() .await .map_err(|err| format!("Profile sync failed: {}", err))?; + if response.status().as_u16() == 401 { + let refresh = client + .post(format!("{}/api/v1/auth/refresh", existing.server_url.trim_end_matches('/'))) + .json(&RefreshRequest { + refresh_token: &existing.refresh_token, + }) + .send() + .await + .map_err(|err| format!("Session refresh failed: {}", err))?; + + if !refresh.status().is_success() { + let status = refresh.status(); + let body = refresh + .text() + .await + .unwrap_or_else(|_| "".into()); + return Err(format!("Session refresh failed with status {}: {}", status, body)); + } + + let refreshed = refresh + .json::() + .await + .map_err(|err| format!("Unable to decode refresh response: {}", err))?; + + existing.access_token = refreshed.access_token; + existing.refresh_token = refreshed.refresh_token; + + response = client + .get(&profile_url) + .bearer_auth(&existing.access_token) + .send() + .await + .map_err(|err| format!("Profile sync failed: {}", err))?; + } + if !response.status().is_success() { let status = response.status(); let body = response