wip
This commit is contained in:
61
rust/Cargo.lock
generated
61
rust/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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<Category>) -> Result<Self, Error> {
|
||||
pub async fn build(state: ClientState, categories: Vec<Category>) -> Result<Self, Error> {
|
||||
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<Category>) -> Result<Self, Error> {
|
||||
pub async fn build(state: &ClientState, categories: &Vec<Category>) -> Result<Self, Error> {
|
||||
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"
|
||||
|
||||
202
rust/src/main.rs
202
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<Sqlite>,
|
||||
client_state: ClientState,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientState {
|
||||
pub active_category_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
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<String>) {
|
||||
|
||||
async fn inventory_active(
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> 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>)> {
|
||||
inventory(None).await
|
||||
async fn inventory_inactive(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
inventory(state, None).await
|
||||
}
|
||||
|
||||
async fn inventory(
|
||||
mut state: AppState,
|
||||
active_id: Option<String>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
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::<Vec<Result<Category, models::Error>>>()
|
||||
.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<String>), (StatusCode, Html<String>)> {
|
||||
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<AppState>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
let trips = query("SELECT * FROM trips")
|
||||
.fetch(&state.database_pool)
|
||||
.map_ok(|row| row.try_into())
|
||||
.try_collect::<Vec<Result<Trip, models::Error>>>()
|
||||
.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(
|
||||
// Path(id): Path<String>,
|
||||
// ) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
@@ -173,7 +308,8 @@ async fn trips() -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>
|
||||
// .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
|
||||
|
||||
Reference in New Issue
Block a user