diff options
Diffstat (limited to 'app/src/lib.rs')
-rw-r--r-- | app/src/lib.rs | 202 |
1 files changed, 72 insertions, 130 deletions
diff --git a/app/src/lib.rs b/app/src/lib.rs index ae80c24..8ad149f 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1,17 +1,18 @@ -mod state; -use state::AppState; +mod oauth; +use oauth::OAuthController; 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; +use uuid::Uuid; -const OAUTH_CLIENT_NAME: &'static str = "foxfleet_test"; +pub const OAUTH_CLIENT_NAME: &'static str = "foxfleet_test"; + +struct AppState { + oauth_controller: OAuthController +} #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -22,7 +23,9 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { - app.manage(Mutex::new(state::AppState::default())); + app.manage(AppState { + oauth_controller: OAuthController::new() + }); #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] app.deep_link().register_all()?; @@ -30,31 +33,37 @@ pub fn run() { 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; - } + domain == "oauth-response" + } else { + false } - 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; - }); - } + let query = ( + query.find(|(key, _)| key == "code").map(|(_, value)| value ), + query.find(|(key, _)| key == "state").map(|(_, value)| value ) + ); + let query = match query { + (Some(code), Some(state)) => Some((code, state)), + _ => None, + }; + + if let Some((auth_code, state)) = query { + if let Ok(state) = Uuid::try_parse(&state) { + let state = state.clone(); + let auth_code = auth_code.to_string().clone(); + let app_handle = app_handle.clone(); + let oauth_controller = app_handle.state::<AppState>().oauth_controller.clone(); + + tauri::async_runtime::spawn(async move { + oauth_controller.resolve_code(state, auth_code); }); - }); + } else { + println!("Invalid UUID format: {state}"); + return + } } else { - println!("No code in oauth callback"); + println!("Missing either state or code in oauth callback"); return } } @@ -66,118 +75,53 @@ pub fn run() { .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<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, +async fn start_account_auth(app_handle: AppHandle, state: State<'_, AppState>, instance_domain: &str) -> Result<Vec<String>, ()> { + let add_result = state.oauth_controller.add_server(instance_domain).await; + + let state_nonce = match add_result { + Ok(result) => { + let opener = app_handle.opener(); + if let Err(_) = opener.open_url(result.auth_url, None::<&str>) { + println!("Could not open authentication page"); + return Err(()) } - } - }) }; - println!("Saved credential"); + result.auth_state + } + Err(err) => { + println!("Error adding server: {err}"); + return Err(()) + } + }; - Ok(()) + let signin_result = state.oauth_controller.finish_signin(state_nonce).await; + match signin_result { + Ok((server_domain, username)) => { + println!("Signed in successfully"); + Ok(vec!(server_domain, username)) + } + Err(err) => { + println!("Error completing signin: {err}"); + Err(()) + } + } } #[tauri::command] -async fn get_self(state: State<'_, Mutex<AppState>>) -> Result<String, String> { +async fn get_self(state: State<'_, AppState>, server_domain: String, username: String) -> 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 + let api_key = state.oauth_controller.get_api_token(server_domain, username).await; + match api_key { + Err(err) => { + println!("Error getting API token: {err}"); + return Err(err) } - }); - - if let Some(account) = account { - if let state::ApiCredential::Some {token, refresh: _} = &account.api_credential { + Ok(api_key) => { if let Ok(result) = client.get("https://social.tempest.dev/api/v1/accounts/verify_credentials") - .bearer_auth(token) + .bearer_auth(api_key) .send().await { if let Ok(result) = result.text().await { return Ok(result) @@ -189,6 +133,4 @@ async fn get_self(state: State<'_, Mutex<AppState>>) -> Result<String, String> { } } } - - return Err("No logged in account".to_string()); } |