mod state; use state::AppState; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_opener::OpenerExt; use std::collections::HashMap; use tauri::{Manager, State, AppHandle}; use url::Host; use serde::Deserialize; use tokio::sync::Mutex; use tokio::sync::mpsc::channel; const OAUTH_CLIENT_NAME: &'static str = "foxfleet_test"; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { println!("{}, {argv:?}, {cwd}", app.package_info().name); })) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { app.manage(Mutex::new(state::AppState::default())); #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] app.deep_link().register_all()?; let app_handle = app.handle().clone(); app.deep_link().on_open_url(move |event| { if let Some(oauth_callback) = event.urls().iter().find(|url| { if let Some(Host::Domain(domain)) = url.host() { if domain == "oauth-response" { return true; } } false }) { let mut query = oauth_callback.query_pairs(); if let Some(code) = query.find(|(key, _value)| key == "code") { let app_handle = app_handle.clone(); let code = code.1.to_string(); tauri::async_runtime::spawn(async move { let app_state = app_handle.state::>(); app_state.lock().await.accounts.iter_mut().for_each(|account| { // TODO: handle if there's multiple of these that match if let state::ApiCredential::Pending(sender) = &account.api_credential { let sender = sender.clone(); let code = code.clone(); tauri::async_runtime::spawn(async move { let _ = sender.send(state::AuthCode(code)).await; }); } }); }); } else { println!("No code in oauth callback"); return } } }); Ok(()) }) .invoke_handler(tauri::generate_handler![start_account_auth, get_self]) .run(tauri::generate_context!()) .expect("Error starting") } #[derive(Deserialize)] struct RegistrationResponse { id: String, name: String, client_id: String, client_secret: String, } #[derive(Deserialize)] struct TokenResponse { access_token: String, created_at: u32, scope: String, token_type: String, } #[tauri::command] async fn start_account_auth(app_handle: AppHandle, state: State<'_, Mutex>, instance_domain: &str) -> Result<(), ()> { println!("Starting account auth"); let registration_endpoint = format!("https://{instance_domain}/api/v1/apps"); let token_endpoint = format!("https://{instance_domain}/oauth/token"); let client = reqwest::Client::builder().user_agent("Foxfleet v0.0.1").build().expect("Could not construct client"); println!("Registering client"); let registration_response : RegistrationResponse = client.post(registration_endpoint) .json(&HashMap::from([ ("client_name", OAUTH_CLIENT_NAME), ("redirect_uris", "dev.tempest.foxfleet://oauth-response"), ("scopes", "read write"), ])).send().await.expect("Could not send client registration") .json().await.expect("Could not parse client registration response"); // Make channel for awaiting let (sender, mut receiver) = channel::(1); println!("Saving registration"); { state.lock().await.accounts.push(state::Account { server_domain: instance_domain.to_string(), handle_domain: None, client_credential: state::ClientCredential { client_name: OAUTH_CLIENT_NAME.to_string(), client_id: registration_response.client_id.clone(), client_secret: Some(registration_response.client_secret.clone()), }, api_credential: state::ApiCredential::Pending(sender), }) } // Open browser to auth page println!("Opening authentication page"); let client_id = registration_response.client_id.clone(); let auth_page = format!("https://{instance_domain}/oauth/authorize?client_id={client_id}&redirect_uri=dev.tempest.foxfleet://oauth-response&response_type=code&scope=read+write"); let opener = app_handle.opener(); if let Err(_) = opener.open_url(auth_page, None::<&str>) { println!("Could not open authentication page"); return Err(()) } // Wait for resolution of the credential let auth_code = receiver.recv().await; if auth_code.is_none() { return Err(()) } let auth_code = auth_code.unwrap(); println!("Exchanging auth code for API token"); // Get long-lived credential let token_response : TokenResponse = client.post(token_endpoint) .json(&HashMap::from([ ("redirect_uri", "dev.tempest.foxfleet://oauth-response"), ("client_id", registration_response.client_id.as_str()), ("client_secret", registration_response.client_secret.as_str()), ("grant_type", "authorization_code"), ("code", auth_code.0.as_str()), ])).send().await.expect("Could not get API token") .json().await.expect("Could not parse client registration response"); println!("Successfully exchanged for credential"); // Save credential { state.lock().await.accounts.iter_mut().for_each(|account| { if account.server_domain == instance_domain { account.api_credential = state::ApiCredential::Some { token: token_response.access_token.clone(), refresh: None, } } }) }; println!("Saved credential"); Ok(()) } #[tauri::command] async fn get_self(state: State<'_, Mutex>) -> Result { let client = reqwest::Client::builder().user_agent("Foxfleet v0.0.1").build().expect("Could not construct client"); let accounts = { state.lock().await.accounts.clone() }; let account = accounts.iter().find(|account| { if let state::ApiCredential::Some {token: _, refresh: _} = account.api_credential { true } else { false } }); if let Some(account) = account { if let state::ApiCredential::Some {token, refresh: _} = &account.api_credential { if let Ok(result) = client.get("https://social.tempest.dev/api/v1/accounts/verify_credentials") .bearer_auth(token) .send().await { if let Ok(result) = result.text().await { return Ok(result) } else { return Err("Error decoding response".to_string()); } } else { return Err("Error fetching account".to_string()); } } } return Err("No logged in account".to_string()); }