Files
hearbit-ai-app/src-tauri/src/auth.rs

113 lines
4.2 KiB
Rust

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<R: Runtime>(app: AppHandle<R>, client_id: String) -> Result<String, String> {
// 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<Vec<serde_json::Value>, 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::<serde_json::Value>()
.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![])
}
}