From 563f1b3d89482043e8dfc85e359eb09bddf5d858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Wed, 10 May 2023 00:42:42 +0200 Subject: [PATCH] wip --- rust/Cargo.lock | 61 +++++++++- rust/Cargo.toml | 6 + rust/src/components/inventory.rs | 36 ++++-- rust/src/main.rs | 202 ++++++++++++++++++++++++++----- 4 files changed, 261 insertions(+), 44 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 03a86ea..535a824 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -50,6 +50,7 @@ dependencies = [ "bitflags", "bytes", "futures-util", + "headers", "http", "http-body", "hyper", @@ -88,6 +89,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" @@ -405,6 +412,31 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -699,6 +731,7 @@ dependencies = [ "futures", "hyper", "maud", + "serde", "sqlx", "time", "tokio", @@ -890,7 +923,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] @@ -926,6 +959,20 @@ name = "serde" version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] [[package]] name = "serde_json" @@ -959,6 +1006,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.6" @@ -1458,6 +1516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 50bff63..29b1c67 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies.axum] version = "0.6.18" +features = ["headers"] [dependencies.tokio] version = "1.28.0" @@ -30,6 +31,7 @@ version = "0.25" version = "1.3.2" features = [ "v4", + "serde", ] [dependencies.sqlx] @@ -41,3 +43,7 @@ version = "0.3.28" [dependencies.time] version = "0.3.21" + +[dependencies.serde] +version = "1.0.162" +features = ["derive"] diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index 57254f9..10b0afc 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -1,7 +1,7 @@ use maud::{html, Markup}; use crate::models::*; -use crate::State; +use crate::ClientState; use uuid::uuid; pub struct Inventory { @@ -9,7 +9,7 @@ pub struct Inventory { } impl Inventory { - pub async fn build(state: State, categories: Vec) -> Result { + pub async fn build(state: ClientState, categories: Vec) -> Result { let doc = html!( div id="pkglist-item-manager" { div ."p-8" ."grid" ."grid-cols-4" ."gap-3" { @@ -198,6 +198,22 @@ impl InventoryItemList { bottom:0; right:0;", width=(item.weight as f32 / biggest_item_weight as f32 * 100.0))) {} } + td + ."border" + ."bg-red-200" + ."hover:bg-red-400" + ."cursor-pointer" + ."w-8" + ."text-center" + { + a + href = (format!("/inventory/item/{id}/delete", id = item.id)) + { + button { + span ."mdi" ."mdi-delete" ."text-xl" {} + } + } + } } } } @@ -221,7 +237,7 @@ pub struct InventoryNewItemForm { } impl InventoryNewItemForm { - pub async fn build(state: &State, categories: &Vec) -> Result { + pub async fn build(state: &ClientState, categories: &Vec) -> Result { let doc = html!( form @@ -238,9 +254,9 @@ impl InventoryNewItemForm { div ."w-11/12" ."mx-auto" { div ."pb-8" { div ."flex" ."flex-row" ."justify-center" ."items-start"{ - label for="item-name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } + label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } span ."w-1/2" { - input type="text" id="item-name" name="name" + input type="text" id="new-item-name" name="new-item-name" ."block" ."w-full" ."p-2" @@ -256,12 +272,12 @@ impl InventoryNewItemForm { } } div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" { - label for="item-weight" .font-bold ."w-1/2" .text-center { "Weight" } + label for="weight" .font-bold ."w-1/2" .text-center { "Weight" } span ."w-1/2" { input type="text" - id="item-weight" - name="weight" + id="new-item-weight" + name="new-item-weight" ."block" ."w-full" ."p-2" @@ -280,8 +296,8 @@ impl InventoryNewItemForm { label for="item-category" .font-bold ."w-1/2" .text-center { "Category" } span ."w-1/2" { select - id="item-category" - name="category" + id="new-item-category-id" + name="new-item-category-id" ."block" ."w-full" ."p-2" diff --git a/rust/src/main.rs b/rust/src/main.rs index 0f9464c..43e886b 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,17 +1,28 @@ #![allow(unused_imports)] use axum::{ extract::Path, - http::StatusCode, - response::Html, + extract::State, + headers, + headers::Header, + http::{header::HeaderMap, StatusCode}, + response::{Html, Redirect}, routing::{get, post}, - Router, + Form, Router, }; -use sqlx::sqlite::SqlitePoolOptions; + +use sqlx::{ + error::DatabaseError, + query, + sqlite::{SqliteConnectOptions, SqliteError, SqlitePoolOptions}, + Pool, Sqlite, +}; + +use serde::Deserialize; use tracing_subscriber; use futures::TryStreamExt; -use uuid::Uuid; +use uuid::{uuid, Uuid}; use std::net::SocketAddr; @@ -21,13 +32,20 @@ mod models; use crate::components::*; use crate::models::*; -pub struct State { +#[derive(Clone)] +pub struct AppState { + database_pool: Pool, + client_state: ClientState, +} + +#[derive(Clone)] +pub struct ClientState { pub active_category_id: Option, } -impl State { +impl ClientState { pub fn new() -> Self { - State { + ClientState { active_category_id: None, } } @@ -39,17 +57,34 @@ async fn main() -> Result<(), sqlx::Error> { .with_max_level(tracing::Level::DEBUG) .init(); + let database_pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with( + SqliteConnectOptions::new() + .filename("/home/hannes-private/sync/items/items.sqlite") + .pragma("foreign_keys", "1"), + ) + .await + .unwrap(); + + let state = AppState { + database_pool, + client_state: ClientState::new(), + }; + // build our application with a route let app = Router::new() .route("/", get(root)) .route("/trips/", get(trips)) .route("/inventory/", get(inventory_inactive)) + .route("/inventory/item/", post(inventory_item_create)) .route("/inventory/category/:id", get(inventory_active)) + .route("/inventory/item/:id/delete", get(inventory_item_delete)) // .route( // "/inventory/category/:id/items", // post(htmx_inventory_category_items), // ); - ; + .with_state(state); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); tracing::debug!("listening on {}", addr); @@ -70,33 +105,30 @@ async fn root() -> (StatusCode, Html) { async fn inventory_active( Path(id): Path, + State(state): State, ) -> Result<(StatusCode, Html), (StatusCode, Html)> { - inventory(Some(id)).await + inventory(state, Some(id)).await } -async fn inventory_inactive() -> Result<(StatusCode, Html), (StatusCode, Html)> { - inventory(None).await +async fn inventory_inactive( + State(state): State, +) -> Result<(StatusCode, Html), (StatusCode, Html)> { + inventory(state, None).await } async fn inventory( + mut state: AppState, active_id: Option, ) -> Result<(StatusCode, Html), (StatusCode, Html)> { - let mut state: State = State::new(); let active_id = active_id .map(|id| Uuid::try_parse(&id)) .transpose() .map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?; - state.active_category_id = active_id; + state.client_state.active_category_id = active_id; - let pool = SqlitePoolOptions::new() - .max_connections(5) - .connect("sqlite:///home/hannes-private/sync/items/items.sqlite") - .await - .unwrap(); - - let mut categories = sqlx::query("SELECT id,name,description FROM inventoryitemcategories") - .fetch(&pool) + let mut categories = query("SELECT id,name,description FROM inventoryitemcategories") + .fetch(&state.database_pool) .map_ok(|row| row.try_into()) .try_collect::>>() .await @@ -126,7 +158,7 @@ async fn inventory( StatusCode::OK, Html::from( Root::build( - Inventory::build(state, categories) + Inventory::build(state.client_state, categories) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))? .into(), @@ -137,15 +169,11 @@ async fn inventory( )) } -async fn trips() -> Result<(StatusCode, Html), (StatusCode, Html)> { - let pool = SqlitePoolOptions::new() - .max_connections(5) - .connect("sqlite:///home/hannes-private/sync/items/items.sqlite") - .await - .unwrap(); - - let trips = sqlx::query("SELECT * FROM trips") - .fetch(&pool) +async fn trips( + State(state): State, +) -> Result<(StatusCode, Html), (StatusCode, Html)> { + let trips = query("SELECT * FROM trips") + .fetch(&state.database_pool) .map_ok(|row| row.try_into()) .try_collect::>>() .await @@ -164,6 +192,113 @@ async fn trips() -> Result<(StatusCode, Html), (StatusCode, Html )) } +#[derive(Deserialize)] +struct NewItem { + #[serde(rename = "new-item-name")] + name: String, + #[serde(rename = "new-item-weight")] + weight: u32, + // damn i just love how serde is integrated everywhere, just add a feature to the uuid in + // cargo.toml and go + #[serde(rename = "new-item-category-id")] + category_id: Uuid, +} + +async fn inventory_item_create( + State(state): State, + Form(new_item): Form, +) -> Result { + query( + "INSERT INTO inventoryitems + (id, name, description, weight, category_id) + VALUES + (?, ?, ?, ?, ?)", + ) + .bind(Uuid::new_v4().to_string()) + .bind(&new_item.name) + .bind("") + .bind(new_item.weight) + .bind(new_item.category_id.to_string()) + .execute(&state.database_pool) + .await + .map_err(|e| match e { + sqlx::Error::Database(ref error) => { + let sqlite_error = error.downcast_ref::(); + if let Some(code) = sqlite_error.code() { + match &*code { + "787" => { + // SQLITE_CONSTRAINT_FOREIGNKEY + ( + StatusCode::BAD_REQUEST, + format!("category {id} not found", id = new_item.category_id), + ) + } + "2067" => { + // SQLITE_CONSTRAINT_UNIQUE + ( + StatusCode::BAD_REQUEST, + format!( + "item with name \"{name}\" already exists in category {id}", + name = new_item.name, + id = new_item.category_id + ), + ) + } + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("got error with unknown code: {}", sqlite_error.to_string()), + ), + } + } else { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("got error without code: {}", sqlite_error.to_string()), + ) + } + } + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("got unknown error: {}", e.to_string()), + ), + })?; + + Ok(Redirect::to(&format!( + "/inventory/category/{id}", + id = new_item.category_id + ))) +} + +async fn inventory_item_delete( + State(state): State, + headers: HeaderMap, + Path(id): Path, +) -> Result { + query( + "DELETE FROM inventoryitems + WHERE id = ?", + ) + .bind(id.to_string()) + .execute(&state.database_pool) + .await + .map_err(|e| ((StatusCode::BAD_REQUEST, e.to_string())))?; + + Ok(Redirect::to( + headers + .get("referer") + .ok_or(( + StatusCode::BAD_REQUEST, + "no referer header found".to_string(), + ))? + .to_str() + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("referer could not be converted: {}", e), + ) + })?, + )) +} + // async fn htmx_inventory_category_items( // Path(id): Path, // ) -> Result<(StatusCode, Html), (StatusCode, Html)> { @@ -173,7 +308,8 @@ async fn trips() -> Result<(StatusCode, Html), (StatusCode, Html // .await // .unwrap(); -// let items = sqlx::query(&format!( +// let items = query(&format!( +// //TODO bind this stuff!!!!!!! no sql injection pls // "SELECT // i.id, i.name, i.description, i.weight, i.category_id // FROM inventoryitemcategories AS c