This commit is contained in:
2023-05-10 00:42:42 +02:00
parent 3264af5c65
commit 563f1b3d89
4 changed files with 261 additions and 44 deletions

61
rust/Cargo.lock generated
View File

@@ -50,6 +50,7 @@ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
"futures-util", "futures-util",
"headers",
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
@@ -88,6 +89,12 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.0" version = "0.21.0"
@@ -405,6 +412,31 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@@ -699,6 +731,7 @@ dependencies = [
"futures", "futures",
"hyper", "hyper",
"maud", "maud",
"serde",
"sqlx", "sqlx",
"time", "time",
"tokio", "tokio",
@@ -890,7 +923,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [ dependencies = [
"base64", "base64 0.21.0",
] ]
[[package]] [[package]]
@@ -926,6 +959,20 @@ name = "serde"
version = "1.0.162" version = "1.0.162"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" 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]] [[package]]
name = "serde_json" name = "serde_json"
@@ -959,6 +1006,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.6" version = "0.10.6"
@@ -1458,6 +1516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde",
] ]
[[package]] [[package]]

View File

@@ -5,6 +5,7 @@ edition = "2021"
[dependencies.axum] [dependencies.axum]
version = "0.6.18" version = "0.6.18"
features = ["headers"]
[dependencies.tokio] [dependencies.tokio]
version = "1.28.0" version = "1.28.0"
@@ -30,6 +31,7 @@ version = "0.25"
version = "1.3.2" version = "1.3.2"
features = [ features = [
"v4", "v4",
"serde",
] ]
[dependencies.sqlx] [dependencies.sqlx]
@@ -41,3 +43,7 @@ version = "0.3.28"
[dependencies.time] [dependencies.time]
version = "0.3.21" version = "0.3.21"
[dependencies.serde]
version = "1.0.162"
features = ["derive"]

View File

@@ -1,7 +1,7 @@
use maud::{html, Markup}; use maud::{html, Markup};
use crate::models::*; use crate::models::*;
use crate::State; use crate::ClientState;
use uuid::uuid; use uuid::uuid;
pub struct Inventory { pub struct Inventory {
@@ -9,7 +9,7 @@ pub struct Inventory {
} }
impl Inventory { impl Inventory {
pub async fn build(state: State, categories: Vec<Category>) -> Result<Self, Error> { pub async fn build(state: ClientState, categories: Vec<Category>) -> Result<Self, Error> {
let doc = html!( let doc = html!(
div id="pkglist-item-manager" { div id="pkglist-item-manager" {
div ."p-8" ."grid" ."grid-cols-4" ."gap-3" { div ."p-8" ."grid" ."grid-cols-4" ."gap-3" {
@@ -198,6 +198,22 @@ impl InventoryItemList {
bottom:0; bottom:0;
right:0;", width=(item.weight as f32 / biggest_item_weight as f32 * 100.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 { impl InventoryNewItemForm {
pub async fn build(state: &State, categories: &Vec<Category>) -> Result<Self, Error> { pub async fn build(state: &ClientState, categories: &Vec<Category>) -> Result<Self, Error> {
let doc = html!( let doc = html!(
form form
@@ -238,9 +254,9 @@ impl InventoryNewItemForm {
div ."w-11/12" ."mx-auto" { div ."w-11/12" ."mx-auto" {
div ."pb-8" { div ."pb-8" {
div ."flex" ."flex-row" ."justify-center" ."items-start"{ 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" { span ."w-1/2" {
input type="text" id="item-name" name="name" input type="text" id="new-item-name" name="new-item-name"
."block" ."block"
."w-full" ."w-full"
."p-2" ."p-2"
@@ -256,12 +272,12 @@ impl InventoryNewItemForm {
} }
} }
div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" { 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" { span ."w-1/2" {
input input
type="text" type="text"
id="item-weight" id="new-item-weight"
name="weight" name="new-item-weight"
."block" ."block"
."w-full" ."w-full"
."p-2" ."p-2"
@@ -280,8 +296,8 @@ impl InventoryNewItemForm {
label for="item-category" .font-bold ."w-1/2" .text-center { "Category" } label for="item-category" .font-bold ."w-1/2" .text-center { "Category" }
span ."w-1/2" { span ."w-1/2" {
select select
id="item-category" id="new-item-category-id"
name="category" name="new-item-category-id"
."block" ."block"
."w-full" ."w-full"
."p-2" ."p-2"

View File

@@ -1,17 +1,28 @@
#![allow(unused_imports)] #![allow(unused_imports)]
use axum::{ use axum::{
extract::Path, extract::Path,
http::StatusCode, extract::State,
response::Html, headers,
headers::Header,
http::{header::HeaderMap, StatusCode},
response::{Html, Redirect},
routing::{get, post}, 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 tracing_subscriber;
use futures::TryStreamExt; use futures::TryStreamExt;
use uuid::Uuid; use uuid::{uuid, Uuid};
use std::net::SocketAddr; use std::net::SocketAddr;
@@ -21,13 +32,20 @@ mod models;
use crate::components::*; use crate::components::*;
use crate::models::*; use crate::models::*;
pub struct State { #[derive(Clone)]
pub struct AppState {
database_pool: Pool<Sqlite>,
client_state: ClientState,
}
#[derive(Clone)]
pub struct ClientState {
pub active_category_id: Option<Uuid>, pub active_category_id: Option<Uuid>,
} }
impl State { impl ClientState {
pub fn new() -> Self { pub fn new() -> Self {
State { ClientState {
active_category_id: None, active_category_id: None,
} }
} }
@@ -39,17 +57,34 @@ async fn main() -> Result<(), sqlx::Error> {
.with_max_level(tracing::Level::DEBUG) .with_max_level(tracing::Level::DEBUG)
.init(); .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 // build our application with a route
let app = Router::new() let app = Router::new()
.route("/", get(root)) .route("/", get(root))
.route("/trips/", get(trips)) .route("/trips/", get(trips))
.route("/inventory/", get(inventory_inactive)) .route("/inventory/", get(inventory_inactive))
.route("/inventory/item/", post(inventory_item_create))
.route("/inventory/category/:id", get(inventory_active)) .route("/inventory/category/:id", get(inventory_active))
.route("/inventory/item/:id/delete", get(inventory_item_delete))
// .route( // .route(
// "/inventory/category/:id/items", // "/inventory/category/:id/items",
// post(htmx_inventory_category_items), // post(htmx_inventory_category_items),
// ); // );
; .with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::debug!("listening on {}", addr); tracing::debug!("listening on {}", addr);
@@ -70,33 +105,30 @@ async fn root() -> (StatusCode, Html<String>) {
async fn inventory_active( async fn inventory_active(
Path(id): Path<String>, Path(id): Path<String>,
State(state): State<AppState>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
inventory(Some(id)).await inventory(state, Some(id)).await
} }
async fn inventory_inactive() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { async fn inventory_inactive(
inventory(None).await State(state): State<AppState>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
inventory(state, None).await
} }
async fn inventory( async fn inventory(
mut state: AppState,
active_id: Option<String>, active_id: Option<String>,
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
let mut state: State = State::new();
let active_id = active_id let active_id = active_id
.map(|id| Uuid::try_parse(&id)) .map(|id| Uuid::try_parse(&id))
.transpose() .transpose()
.map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?; .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() let mut categories = query("SELECT id,name,description FROM inventoryitemcategories")
.max_connections(5) .fetch(&state.database_pool)
.connect("sqlite:///home/hannes-private/sync/items/items.sqlite")
.await
.unwrap();
let mut categories = sqlx::query("SELECT id,name,description FROM inventoryitemcategories")
.fetch(&pool)
.map_ok(|row| row.try_into()) .map_ok(|row| row.try_into())
.try_collect::<Vec<Result<Category, models::Error>>>() .try_collect::<Vec<Result<Category, models::Error>>>()
.await .await
@@ -126,7 +158,7 @@ async fn inventory(
StatusCode::OK, StatusCode::OK,
Html::from( Html::from(
Root::build( Root::build(
Inventory::build(state, categories) Inventory::build(state.client_state, categories)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))? .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?
.into(), .into(),
@@ -137,15 +169,11 @@ async fn inventory(
)) ))
} }
async fn trips() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { async fn trips(
let pool = SqlitePoolOptions::new() State(state): State<AppState>,
.max_connections(5) ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
.connect("sqlite:///home/hannes-private/sync/items/items.sqlite") let trips = query("SELECT * FROM trips")
.await .fetch(&state.database_pool)
.unwrap();
let trips = sqlx::query("SELECT * FROM trips")
.fetch(&pool)
.map_ok(|row| row.try_into()) .map_ok(|row| row.try_into())
.try_collect::<Vec<Result<Trip, models::Error>>>() .try_collect::<Vec<Result<Trip, models::Error>>>()
.await .await
@@ -164,6 +192,113 @@ async fn trips() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>
)) ))
} }
#[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<AppState>,
Form(new_item): Form<NewItem>,
) -> Result<Redirect, (StatusCode, String)> {
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::<SqliteError>();
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<AppState>,
headers: HeaderMap,
Path(id): Path<Uuid>,
) -> Result<Redirect, (StatusCode, String)> {
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( // async fn htmx_inventory_category_items(
// Path(id): Path<String>, // Path(id): Path<String>,
// ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> { // ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
@@ -173,7 +308,8 @@ async fn trips() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>
// .await // .await
// .unwrap(); // .unwrap();
// let items = sqlx::query(&format!( // let items = query(&format!(
// //TODO bind this stuff!!!!!!! no sql injection pls
// "SELECT // "SELECT
// i.id, i.name, i.description, i.weight, i.category_id // i.id, i.name, i.description, i.weight, i.category_id
// FROM inventoryitemcategories AS c // FROM inventoryitemcategories AS c