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.
This commit is contained in:
2026-03-18 09:23:52 +01:00
parent d5c6760a2d
commit d1940e6f28

View File

@@ -75,6 +75,11 @@ struct LoginRequest<'a> {
password: &'a str, password: &'a str,
} }
#[derive(Debug, Serialize)]
struct RefreshRequest<'a> {
refresh_token: &'a str,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct LoginResponse { struct LoginResponse {
#[serde(rename = "access_token")] #[serde(rename = "access_token")]
@@ -406,7 +411,7 @@ fn current_metrics(app: &AppHandle) -> Result<TunnelMetrics, String> {
} }
async fn sync_current_session(app: &AppHandle) -> Result<SessionState, String> { async fn sync_current_session(app: &AppHandle) -> Result<SessionState, String> {
let existing = { let mut existing = {
let state = app.state::<AppState>(); let state = app.state::<AppState>();
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?; 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())? 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<SessionState, String> {
.build() .build()
.map_err(|err| err.to_string())?; .map_err(|err| err.to_string())?;
let response = client let profile_url = format!("{}/api/v1/me/profile", existing.server_url.trim_end_matches('/'));
.get(format!("{}/api/v1/me/profile", existing.server_url.trim_end_matches('/'))) let mut response = client
.get(&profile_url)
.bearer_auth(&existing.access_token) .bearer_auth(&existing.access_token)
.send() .send()
.await .await
.map_err(|err| format!("Profile sync failed: {}", err))?; .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(|_| "<unable to read response body>".into());
return Err(format!("Session refresh failed with status {}: {}", status, body));
}
let refreshed = refresh
.json::<LoginResponse>()
.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() { if !response.status().is_success() {
let status = response.status(); let status = response.status();
let body = response let body = response