feat: complete history, attendees list, and smart templates
This commit is contained in:
112
src-tauri/src/auth.rs
Normal file
112
src-tauri/src/auth.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
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![])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user