113 lines
4.2 KiB
Rust
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![])
|
|
}
|
|
}
|