diff options
Diffstat (limited to 'app/src/lib.rs')
-rw-r--r-- | app/src/lib.rs | 186 |
1 files changed, 183 insertions, 3 deletions
diff --git a/app/src/lib.rs b/app/src/lib.rs index d9ad08c..ae80c24 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1,14 +1,194 @@ +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()) - .invoke_handler(tauri::generate_handler![greet]) + .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::<Mutex<AppState>>(); + 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] -fn greet(name: &str) -> String { - format!("Hello, {}!", name) +async fn start_account_auth(app_handle: AppHandle, state: State<'_, Mutex<AppState>>, 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::<state::AuthCode>(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<AppState>>) -> Result<String, String> { + 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()); +} |