summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorAshelyn Rose <git@ashen.earth>2025-02-16 15:18:09 -0700
committerAshelyn Rose <git@ashen.earth>2025-02-16 15:18:09 -0700
commit5e8d3bc7008d29115bc520a75a9e49c00e2c270f (patch)
treedc3aab4ba61ff0b558cdee8cbe08e07533be5596 /app
parentb5d6d25912993b91bc1b3ec52c352431398c36d9 (diff)
Can now sign in and fetch account data
Diffstat (limited to 'app')
-rw-r--r--app/Cargo.toml9
-rw-r--r--app/Tauri.toml5
-rw-r--r--app/src/lib.rs186
-rw-r--r--app/src/state.rs46
4 files changed, 241 insertions, 5 deletions
diff --git a/app/Cargo.toml b/app/Cargo.toml
index 45b24b1..f11ae44 100644
--- a/app/Cargo.toml
+++ b/app/Cargo.toml
@@ -11,7 +11,12 @@ crate-type = ["staticlib", "cdylib", "rlib"]
 tauri-build = { version = "2", features = [] }
 
 [dependencies]
-tauri = { version = "2", features = ["config-toml"] }
-tauri-plugin-opener = "2"
+reqwest = { version = "0.12.12", features = ["json"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
+tauri = { version = "2", features = ["config-toml"] }
+tauri-plugin-opener = "2"
+tauri-plugin-deep-link = "2"
+tauri-plugin-single-instance = {version = "2", features = ["deep-link"] }
+tokio = "1.43.0"
+url = "2.5.4"
diff --git a/app/Tauri.toml b/app/Tauri.toml
index ad2aa7f..7617202 100644
--- a/app/Tauri.toml
+++ b/app/Tauri.toml
@@ -17,3 +17,8 @@ height = 600
 
 [bundle]
 active = true
+
+[plugins.deep-link.desktop]
+schemes = [
+  "dev.tempest.foxfleet"
+]
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());
+}
diff --git a/app/src/state.rs b/app/src/state.rs
new file mode 100644
index 0000000..44e74ed
--- /dev/null
+++ b/app/src/state.rs
@@ -0,0 +1,46 @@
+use tokio::sync::mpsc::Sender;
+
+#[derive(Clone)]
+pub struct AppState {
+    pub preferences: (),
+    pub accounts: Vec<Account>,
+}
+
+impl AppState {
+    pub fn default() -> Self {
+        Self {
+            preferences: (),
+            accounts: Vec::new(),
+
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct Account {
+    pub server_domain: String,
+    pub handle_domain: Option<String>,
+    pub client_credential: ClientCredential,
+    pub api_credential: ApiCredential,
+}
+
+#[derive(Clone)]
+pub struct ClientCredential {
+    pub client_name: String,
+    pub client_id: String,
+    pub client_secret: Option<String>,
+}
+
+#[derive(Clone)]
+pub struct AuthCode (pub String);
+
+#[derive(Clone)]
+pub enum ApiCredential {
+    None,
+    Pending(Sender<AuthCode>),
+    Some {
+        token: String,
+        refresh: Option<String>
+    }
+}
+