use tauri::{AppHandle, Runtime}; use tauri_plugin_opener::OpenerExt; use tauri_plugin_oauth::start; use url::Url; use oauth2::{ basic::BasicClient, AuthUrl, ClientId, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use oauth2::reqwest::async_http_client; // const CLIENT_ID: &str = "YOUR_CLIENT_ID_HERE"; const AUTH_URL: &str = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; const TOKEN_URL: &str = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; #[tauri::command] pub async fn start_auth_flow(app: AppHandle, client_id: String) -> Result { // 1. Start Localhost Server let (tx, rx) = std::sync::mpsc::channel(); // tauri-plugin-oauth start() returns a port and stops server when callback received let port = start(move |url| { // Ignore favicon requests to avoid triggering early if url.contains("favicon.ico") { return; } let _ = tx.send(url); }) .map_err(|e| format!("Failed to start oauth server: {}", e))?; let redirect_uri = format!("http://localhost:{}/auth/callback", port); // 2. Setup OAuth Client let client = BasicClient::new( ClientId::new(client_id), None, // No client secret for PKCE public client AuthUrl::new(AUTH_URL.to_string()).map_err(|e| e.to_string())?, Some(TokenUrl::new(TOKEN_URL.to_string()).map_err(|e| e.to_string())?), ) .set_redirect_uri(RedirectUrl::new(redirect_uri.clone()).map_err(|e| e.to_string())?); // 3. Generate PKCE Challenge let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); // 4. Generate Auth URL let (auth_url, _csrf_token) = client .authorize_url(CsrfToken::new_random) .add_scope(Scope::new("User.Read".to_string())) .add_scope(Scope::new("Calendars.Read".to_string())) .set_pkce_challenge(pkce_challenge) .url(); // 5. Open Browser app.opener().open_url(auth_url.as_str(), None::<&str>) .map_err(|e| format!("Failed to open browser: {}", e))?; // 6. Wait for Callback let received_url_str = rx.recv().map_err(|e| format!("Failed to receive auth code: {}", e))?; // 7. Parse Code from URL // Actually we need to parse the query params from received_url_str let parsed_url = Url::parse(&received_url_str).map_err(|e| e.to_string())?; let pairs: std::collections::HashMap<_, _> = parsed_url.query_pairs().into_owned().collect(); if let Some(err) = pairs.get("error") { let desc = pairs.get("error_description").map(|s| s.as_str()).unwrap_or("No description"); return Err(format!("OAuth Error: {} ({})", err, desc)); } let code = pairs.get("code").ok_or_else(|| format!("No code in redirect callback. Received URL: {}", received_url_str))?; // 8. Exchange Code for Token let token_result = client .exchange_code(oauth2::AuthorizationCode::new(code.clone())) .set_pkce_verifier(pkce_verifier) .request_async(async_http_client) .await .map_err(|e| format!("Failed to exchange token: {}", e))?; let access_token = token_result.access_token().secret(); // Save token? Or just return it. // Ideally we save it in key storage, but for MVP return it. Ok(access_token.clone()) } #[tauri::command] pub async fn get_calendar_events(token: String) -> Result, String> { let client = reqwest::Client::new(); let res = client .get("https://graph.microsoft.com/v1.0/me/calendarView") .bearer_auth(token) .query(&[ ("startDateTime", chrono::Utc::now().to_rfc3339()), ("endDateTime", (chrono::Utc::now() + chrono::Duration::days(7)).to_rfc3339()), ("$select", "id,subject,start,end,location,onlineMeeting,bodyPreview,body,attendees".to_string()) ]) .header("Prefer", "outlook.timezone=\"UTC\"") .send() .await .map_err(|e| e.to_string())? .json::() .await .map_err(|e| e.to_string())?; // Extract 'value' array if let Some(events) = res.get("value").and_then(|v| v.as_array()) { Ok(events.clone()) } else { Ok(vec![]) } }