diff --git a/unshell-gui/src/auth/mod.rs b/unshell-gui/src/auth/mod.rs index 1937a86..fe19aa0 100644 --- a/unshell-gui/src/auth/mod.rs +++ b/unshell-gui/src/auth/mod.rs @@ -1,7 +1,7 @@ use egui::{Align2, Area, Color32, Frame, Order, Sense, UiKind, Vec2, mutex::Mutex}; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde::de::DeserializeOwned; use serde_json::json; -use std::{marker::PhantomData, sync::Arc}; +use std::sync::Arc; use wasm_bindgen::prelude::Closure; #[derive(serde::Deserialize, serde::Serialize)] @@ -152,7 +152,7 @@ impl Auth { pub fn get(&self, path: &str, ret: F) where - F: FnOnce(Result) + 'static, + F: FnOnce(T) + 'static, T: DeserializeOwned, { if let Some(ref token) = self.token { @@ -163,7 +163,7 @@ impl Auth { Closure::once_into_js(move |ok: bool, response: String| { if ok { crate::log(&response); - if let Ok(value) = serde_json::from_str::>(&response) { + if let Ok(value) = serde_json::from_str::(&response) { ret(value) } else { *(state.lock()) = AuthState::Error("Malformed Response".into()); diff --git a/unshell-gui/src/config/mod.rs b/unshell-gui/src/config/mod.rs index 1ad1dba..95e1897 100644 --- a/unshell-gui/src/config/mod.rs +++ b/unshell-gui/src/config/mod.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; use egui::Color32; @@ -6,29 +9,138 @@ use crate::auth::Auth; #[derive(serde::Deserialize, serde::Serialize)] pub struct Config { - response_text: Arc>, + tree_option: String, + #[serde(skip)] + state: Arc>, + // trees: Arc>>>, + // #[serde(skip)] + // is_requesting: Arc, +} + +#[derive(Default)] +struct ConfigState { + trees: Option>, + tree_keys: Option>, + is_requesting: bool, } impl Default for Config { fn default() -> Self { Self { - response_text: Arc::new(Mutex::new("NONE".to_string())), + tree_option: "".into(), + state: Arc::new(Mutex::new(ConfigState { + trees: None, + tree_keys: None, + is_requesting: false, + })), // trees: Arc::new(Mutex::new(None)), + // is_requesting: Arc::new(AtomicBool::new(false)), } } } impl Config { pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) { - if ui.button("Set Value").clicked() { - let text_clone = self.response_text.clone(); - auth.get("/api/test", move |response: Result| { - *text_clone.lock().unwrap() = format!("{:?}", response); - }); + let mut state_lock = self.state.lock().unwrap(); + let tree_list_none = state_lock.trees.is_none(); + let key_list_none = state_lock.tree_keys.is_none(); + let is_requesting = state_lock.is_requesting; + + if !tree_list_none + && !state_lock + .trees + .as_ref() + .unwrap() + .contains(&self.tree_option) + { + self.tree_option.clear(); + } + + drop(state_lock); + + if ui.button("Refresh").clicked() { + self.tree_option.clear(); + (*self.state.lock().unwrap()).trees = None; } ui.horizontal(|ui| { - ui.label("Response: "); - ui.colored_label(Color32::WHITE, &*self.response_text.lock().unwrap()); + if tree_list_none && !is_requesting { + self.state.lock().unwrap().is_requesting = true; + let state_clone = self.state.clone(); + + auth.get("/api/trees", move |response: Vec| { + let mut state_lock = state_clone.lock().unwrap(); + state_lock.trees = Some(response); + state_lock.is_requesting = false; + drop(state_lock); + }); + } else if tree_list_none && is_requesting { + ui.spinner(); + } }); + + ui.horizontal(|ui| { + if !tree_list_none { + // let tree_len = self.state.lock().unwrap().trees.as_ref().unwrap().len(); + + // for tree in &state_lock.unwrap() { + // if ui.button(tree).clicked() { + // self.tree_option = tree.to_string(); + // (*state_lock).tree_keys.as_mut(); + // } + // } + + let state_lock = self.state.lock().unwrap(); + let trees = state_lock.trees.as_ref().unwrap().clone(); + drop(state_lock); + + for tree in trees { + // let tree: &&String = self + // .state + // .lock() + // .unwrap() + // .tree_keys + // .unwrap() + // .iter() + // .nth(i) + // .unwrap(); + + // let tree sel + + if ui.button(&tree).clicked() { + self.tree_option = tree.to_string(); + (*self.state.lock().unwrap()).tree_keys = None; + } + } + } + }); + + if !self.tree_option.is_empty() && !tree_list_none { + ui.horizontal(|ui| { + ui.label(&format!("Tree: {}", self.tree_option)); + }); + + ui.horizontal(|ui| { + if key_list_none && !is_requesting { + self.state.lock().unwrap().is_requesting = true; + let state_clone = self.state.clone(); + + auth.get( + &format!("/api/keys/{}", self.tree_option), + move |response: Result, String>| { + let mut state_lock = state_clone.lock().unwrap(); + state_lock.tree_keys = Some(response.unwrap()); + state_lock.is_requesting = false; + }, + ); + } else if key_list_none && is_requesting { + ui.spinner(); + } else { + ui.label(&format!( + "Keys: {:?}", + self.state.lock().unwrap().tree_keys.clone() + )); + } + }); + } } } diff --git a/unshell-server/src/api/app.rs b/unshell-server/src/api/app.rs index 61caef9..84de897 100644 --- a/unshell-server/src/api/app.rs +++ b/unshell-server/src/api/app.rs @@ -2,11 +2,10 @@ use axum::{ Extension, Json, Router, extract::{Path, State}, middleware, - response::IntoResponse, routing::{get, post}, }; use tokio::net::TcpListener; -use unshell_lib::info; +use unshell_lib::{debug, info}; use crate::{ api::{auth, structs::CurrentUser}, @@ -20,47 +19,85 @@ pub async fn start_api(address: &str, database: Database) { info!("Listening on {}", listener.local_addr().unwrap()); - let app = Router::new() - .route("/api/auth", post(auth::sign_in)) - .route( - "/api/{*path}", - get(get_data).layer(middleware::from_fn(auth::authorize)), - ) - .route( - "/api/{*path}", - post(post_data).layer(middleware::from_fn(auth::authorize)), - ) - .with_state(database); + let mut router = Router::new().route("/api/auth", post(auth::sign_in)); + router = route_get_trees(router); + router = route_get_keys(router); + router = route_trees(router); - axum::serve(listener, app) + axum::serve(listener, router.with_state(database)) .await .expect("Error serving application"); } -pub async fn get_data( - State(database): State, - Path(path): Path, - Extension(_): Extension, -) -> impl IntoResponse { - let result = database.get_value(&path); +// Route the "keys" api for each tree +fn route_get_trees(router: Router) -> Router { + router.route( + "/api/trees", + get( + async |State(database): State, Extension(_): Extension| { + debug!("GET /api/trees"); + let result = database.get_trees(); - Json(serde_json::to_value(result).unwrap()) + Json(serde_json::to_value(result).unwrap()) + }, + ) + .layer(middleware::from_fn(auth::authorize)), + ) } -pub async fn post_data( - State(database): State, - Path(path): Path, - Extension(_): Extension, - body: String, -) -> impl IntoResponse { - let result = database.put_value(&path, &body); +// Route the "keys" api for each tree +fn route_get_keys(router: Router) -> Router { + router.route( + "/api/keys/{*path}", + get( + async |State(database): State, + Path(path): Path, + Extension(_): Extension| { + debug!("GET /api/keys/{}", path); + let result = database.get_keys(&path); - Json(serde_json::to_value(result).unwrap()) + Json(serde_json::to_value(result).unwrap()) + }, + ) + .layer(middleware::from_fn(auth::authorize)), + ) } -// impl IntoResponse for Option { -// // impl IntoResponse for Option { -// fn into_response(self) -> axum::response::Response { -// todo!() -// } -// } +// Loop through all trees and add /api// POST aand GET listeners for them +fn route_trees(mut router: Router) -> Router { + for tree in crate::DATABASE_TREES.iter() { + router = router + // Route GET requests to this tree + .route( + &format!("/api/{}/{{*path}}", tree), + get( + async |State(database): State, + Path(path): Path, + Extension(_): Extension| { + let result = database.get_value(tree, &path); + debug!("GET /api/{}/{}", tree.to_string(), path); + + Json(serde_json::to_value(result).unwrap()) + }, + ) + .layer(middleware::from_fn(auth::authorize)), + ) + // Route POST requests to this tree + .route( + &format!("/api/{}/{{*path}}", tree), + post( + async |State(database): State, + Path(path): Path, + Extension(_): Extension, + body: String| { + let result = database.put_value(tree, &path, &body); + debug!("POST /api/{}/{}", tree.to_string(), path); + + Json(serde_json::to_value(result).unwrap()) + }, + ) + .layer(middleware::from_fn(auth::authorize)), + ); + } + router +} diff --git a/unshell-server/src/database.rs b/unshell-server/src/database.rs index 13fe6aa..89ee0f7 100644 --- a/unshell-server/src/database.rs +++ b/unshell-server/src/database.rs @@ -1,3 +1,4 @@ +use sled::Tree; use unshell_lib::error; #[derive(Clone)] @@ -12,26 +13,59 @@ impl Database { } } - pub fn put_value(&self, key: &str, value: &str) -> Result<(), String> { - match self.db.insert(key, value) { + fn get_tree(&self, tree_name: &str) -> Result { + self.db.open_tree(tree_name).map_err(|e| { + error!("DB Failed to open tree: {}", e); + "Internal server error".to_string() + }) + } + + pub fn get_trees(&self) -> Vec { + self.db + .tree_names() + .iter() + .map(|n| String::from_utf8_lossy(&n.to_vec()).to_string()) + .collect::>() + } + + pub fn get_keys(&self, tree_name: &str) -> Result, String> { + Ok(self + .get_tree(tree_name)? + .iter() + .keys() + .map(|key| { + String::from_utf8_lossy(&key.expect("This key should exist").to_vec()).to_string() + }) + .collect::>()) + } + + pub fn put_value(&self, tree_name: &str, key: &str, value: &str) -> Result<(), String> { + match self.get_tree(tree_name)?.insert(key, value) { Ok(_) => Ok(()), Err(e) => { error!("Failed to load '{}' from database: {}", key, e); - Err(e.to_string()) + Err("Internal server error".to_string()) } } } - pub fn get_value(&self, key: &str) -> Result { - match self.db.get(key) { + pub fn get_value(&self, tree_name: &str, key: &str) -> Result { + match self.get_tree(tree_name)?.get(key) { Ok(v) => match v { Some(v) => Ok(String::from_utf8_lossy(&v.to_vec()).to_string()), None => Err(format!("Could not find key '{}'", key)), }, Err(e) => { error!("Failed to load '{}' from database: {}", key, e); - Err(e.to_string()) + Err("Internal server error".to_string()) + // Err(e.to_string()) } } } } + +impl Drop for Database { + fn drop(&mut self) { + self.db.flush().expect("Failed to flush database on drop"); + } +} diff --git a/unshell-server/src/lib.rs b/unshell-server/src/lib.rs index e3877fe..0db17f8 100644 --- a/unshell-server/src/lib.rs +++ b/unshell-server/src/lib.rs @@ -3,3 +3,6 @@ mod api; pub mod database; pub use api::app::start_api; + +#[static_init::dynamic] +static DATABASE_TREES: Vec<&'static str> = vec!["users"];